MCP Server Security: How I Discovered a Supply Chain Attack Vector
My Cursor IDE crashed. Machine froze. Then I saw the logs: uvx had auto-downloaded a malicious version of LiteLLM that was uploaded to PyPI just minutes earlier.
The scary part? I didn’t run any command. Cursor auto-loaded my MCP server, which triggered uvx to fetch dependencies. One unpinned dependency was all it took.
The Attack Chain
Cursor autoloads MCP | v uvx runs | v Checks pyproject.toml | v Finds litellm>=1.50.0 (unpinned!) | v Downloads litellm@latest (1.82.8 - MALICIOUS) | v .pth file executes on install | v Credentials harvested + fork bomb | v Machine crashesTimeline of the attack:
- 10:52 UTC - Malware uploaded to PyPI
- 10:54 UTC - My Cursor IDE auto-loaded the MCP server
- 10:54 UTC - uvx pulled the latest litellm (malicious)
- 10:55 UTC - .pth file executed automatically
- 10:55 UTC - Machine crashed due to fork bomb bug in malware
The malware was fresh. No security tool had seen it yet.
Why MCP Servers Are Dangerous
MCP (Model Context Protocol) servers run with your full permissions. They can:
File System Access - Read your source code - Write to any directory - Access .env files
Network Access - Call external APIs - Upload files - Exfiltrate data
Environment Access - Read all env vars - Access API keys - See cloud credentials
Process Execution - Run arbitrary commands - Install packages - Modify system settingsWhen uvx or npx downloads dependencies automatically:
- No user confirmation required
- Latest version by default
- No hash verification
- Full system access
This is Simon Willison’s “lethal trifecta”: AI tools that can access private data, take actions on your behalf, and process untrusted content.
My MCP Configuration (The Problem)
I had this in my settings:
{ "mcpServers": { "my-server": { "command": "uvx", "args": ["my-mcp-server"], "env": {} } }}The pyproject.toml in that server had:
[project]dependencies = [ "litellm>=1.50.0", # UNPINNED!]When Cursor loaded this configuration:
- uvx checked for litellm
- Found version specifier
>=1.50.0 - Resolved to “latest” (1.82.8)
- Downloaded and installed the malicious package
- The .pth file ran automatically during install
No user interaction. No confirmation dialog. Just automatic compromise.
First Attempt: Pinning Dependencies
I tried pinning versions:
{ "mcpServers": { "my-server": { "command": "uvx", "args": ["my-mcp-server==1.2.3"], "env": { "UV_NO_CACHE": "1" } } }}But this only pins the MCP server itself, not its dependencies. The transitive dependency problem remains.
Second Attempt: Lock Files
I checked if the MCP server had a lock file:
cd my-mcp-server/ls -la | grep -E "(uv.lock|requirements.lock|poetry.lock)"
# Nothing found!No lock file means every install could fetch different versions of dependencies.
I created one:
cd my-mcp-server/uv lockThe resulting uv.lock includes cryptographic hashes:
[[package]]name = "litellm"version = "1.82.6"source = { registry = "https://pypi.org/simple" }sdist = { hash = { sha256 = "abc123def456..." } }wheels = [ { url = "...", hash = { sha256 = "def456..." } }]Now installation verifies hashes. But I still wasn’t comfortable.
The Real Solution: Remote MCP Architecture
I realized the fundamental problem: local MCP servers run on my machine with my permissions.
The solution? Run MCP servers remotely.
{ "mcpServers": { "my-remote-server": { "url": "https://mcp.example.com/mcp", "transport": "http" } }}Why this is safer:
LOCAL MCP (Dangerous) REMOTE MCP (Safe)---------------------- ------------------Runs on your machine --> Runs on isolated serverYour permissions --> Restricted permissionsFull filesystem access --> No filesystem accessAll env vars visible --> Only provided env varsNetwork unrestricted --> Network controlledDependencies auto-fetch --> Audited dependenciesI migrated my MCP server to a remote architecture:
- Host the MCP server on a VPS
- Use HTTPS with authentication
- Dependencies are pinned and audited
- Server runs as unprivileged user
- Network access is restricted via firewall
Now even if a dependency is compromised, the blast radius is limited.
Understanding uvx Auto-Download Behavior
The root cause was uvx’s auto-download. Here’s what happens:
# When you run: uvx my-mcp-serveruvx my-mcp-server | +--> Check if tool is installed | (not found) | +--> Create isolated environment | +--> Resolve dependencies | | | +--> Read pyproject.toml | | | +--> Resolve version constraints | litellm>=1.50.0 --> litellm==1.82.8 (LATEST!) | +--> Download packages (no hash verification!) | +--> Install packages (.pth files execute!) | +--> Run the MCP serverThe problem: version specifiers like >=1.50.0 or ~= resolve to the latest version, which could be malicious.
Detecting Unpinned Dependencies
I wrote a script to audit my MCP servers:
#!/bin/bash
echo "=== MCP Server Dependency Audit ==="
find ~/.config -name "mcp_config.json" 2>/dev/null | while read config; do echo "" echo "Config: $config" cat "$config" | jq -r '.mcpServers | to_entries[] | "\(.key): \(.value.command)"' 2>/dev/nulldone
echo ""echo "=== Checking for unpinned dependencies ==="
find ~/.local/share/uv -name "pyproject.toml" 2>/dev/null | while read p; do dir=$(dirname "$p") echo "" echo "Project: $dir"
# Check for version specifiers grep -E "(>=|<=|>|<|~=|\*)" "$p" 2>/dev/null && \ echo "[WARNING] Unpinned dependency found!"
# Check for lock file if [ ! -f "$dir/uv.lock" ] && [ ! -f "$dir/poetry.lock" ]; then echo "[WARNING] No lock file found!" fidoneOutput:
=== MCP Server Dependency Audit ===
Config: ~/.config/claude-code/mcp_config.jsoncontext7: uvxmy-server: uvx
=== Checking for unpinned dependencies ===
Project: ~/.local/share/uv/tools/my-serverlitellm>=1.50.0[WARNING] Unpinned dependency found![WARNING] No lock file found!Restricting Local MCP Servers
If you must use local MCP servers, restrict them:
# Create a non-admin usersudo sysadminctl -addUser mcp_runner -password "$(openssl rand -base64 32)" -admin no
# Set restricted home directorysudo createhomedir -c -u mcp_runner
# Limit network access (macOS)sudo /usr/libexec/ApplicationFirewall/socketfilterfw --blockapp /Users/mcp_runner/.local/bin/uvxThen run MCP servers as that user:
{ "mcpServers": { "my-server": { "command": "sudo", "args": ["-u", "mcp_runner", "uvx", "my-mcp-server==1.2.3"] } }}But this is complex. Remote MCP is simpler.
Network Isolation for Local MCP
Another approach: block network access for uvx:
# Create a socket filter rulesudo /usr/libexec/ApplicationFirewall/socketfilterfw --add $(which uvx)sudo /usr/libexec/ApplicationFirewall/socketfilterfw --blockapp $(which uvx)This prevents data exfiltration, but also blocks legitimate API calls. Trade-offs.
On Linux, use firejail:
firejail --noprofile --net=none uvx my-mcp-serverThe Complete Security Checklist
[ ] Prefer remote MCP servers over local[ ] If local, pin all dependency versions[ ] Generate and commit lock files (uv.lock)[ ] Audit dependencies before use[ ] Run MCP servers as unprivileged users[ ] Restrict network access where possible[ ] Never store credentials in MCP server code[ ] Review MCP server code before using[ ] Check GitHub releases match PyPI versions[ ] Set UV_NO_CACHE=1 to prevent stale cacheWhy This Matters
MCP servers are becoming standard for AI tools. Claude Code, Cursor, and others rely on them. But the current ecosystem has risks:
- Many MCP servers are unmaintained - Dependencies go stale
- No standard security model - Each server handles auth differently
- Auto-download is the default - uvx/npx fetch without asking
- Full system access - MCP runs with your permissions
The LiteLLM attack demonstrated the timing vulnerability: malware uploaded at 10:52, compromised systems by 10:55. Three minutes.
Related Incidents
This isn’t isolated. Similar supply chain attacks have occurred:
- event-stream (2018) - Popular npm package, stole cryptocurrency
- ua-parser-js (2021) - npm package, installed cryptominers
- py-crypto (2022) - PyPI typo-squatting attack
- colors.js (2022) - npm protestware, broke CI pipelines
Each exploited automatic dependency resolution.
Quick Reference
Secure MCP configuration:
{ "mcpServers": { "secure-remote": { "url": "https://your-server.com/mcp", "transport": "http", "headers": { "Authorization": "Bearer your-token" } }, "pinned-local": { "command": "uvx", "args": ["your-server==1.2.3"], "env": { "UV_NO_CACHE": "1", "UV_SYSTEM_PYTHON": "1" } } }}Audit your MCP servers:
# Check MCP configurationscat ~/.config/claude-code/mcp_config.jsoncat ~/.cursor/mcp.json
# Check installed MCP toolsls -la ~/.local/share/uv/tools/
# Audit dependenciesfind ~/.local/share/uv/tools -name "pyproject.toml" -exec cat {} \;Summary
MCP servers introduce supply chain risk through their dependencies. The LiteLLM attack showed how unpinned dependencies can lead to compromise within minutes of malware appearing on PyPI.
My key learnings:
- Remote MCP is safer - Server runs on controlled infrastructure, not your machine
- Pin everything - Use exact versions and lock files with hashes
- Audit before using - Check MCP server code and dependencies
- Restrict permissions - Run as unprivileged user if local
- Block network access - Prevent data exfiltration
The convenience of automatic MCP loading comes with real risks. A three-minute window between malware upload and system compromise is not enough time for any security tool to respond.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Model Context Protocol Specification
- 👨💻 Simon Willison's Lethal Trifecta
- 👨💻 uvx Documentation
- 👨💻 Claude Code MCP Configuration
- 👨💻 Cursor MCP Setup
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments