Skip to content

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

How the compromise happened
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 crashes

Timeline of the attack:

  1. 10:52 UTC - Malware uploaded to PyPI
  2. 10:54 UTC - My Cursor IDE auto-loaded the MCP server
  3. 10:54 UTC - uvx pulled the latest litellm (malicious)
  4. 10:55 UTC - .pth file executed automatically
  5. 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:

MCP server capabilities
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 settings

When 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:

mcp_config.json - BEFORE (dangerous)
{
"mcpServers": {
"my-server": {
"command": "uvx",
"args": ["my-mcp-server"],
"env": {}
}
}
}

The pyproject.toml in that server had:

pyproject.toml - The vulnerability
[project]
dependencies = [
"litellm>=1.50.0", # UNPINNED!
]

When Cursor loaded this configuration:

  1. uvx checked for litellm
  2. Found version specifier >=1.50.0
  3. Resolved to “latest” (1.82.8)
  4. Downloaded and installed the malicious package
  5. The .pth file ran automatically during install

No user interaction. No confirmation dialog. Just automatic compromise.

First Attempt: Pinning Dependencies

I tried pinning versions:

mcp_config.json - With version pinning
{
"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:

Checking for lock files
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:

Creating lock file
cd my-mcp-server/
uv lock

The resulting uv.lock includes cryptographic hashes:

uv.lock - With 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.

mcp_config.json - Remote server (recommended)
{
"mcpServers": {
"my-remote-server": {
"url": "https://mcp.example.com/mcp",
"transport": "http"
}
}
}

Why this is safer:

Local vs Remote MCP comparison
LOCAL MCP (Dangerous) REMOTE MCP (Safe)
---------------------- ------------------
Runs on your machine --> Runs on isolated server
Your permissions --> Restricted permissions
Full filesystem access --> No filesystem access
All env vars visible --> Only provided env vars
Network unrestricted --> Network controlled
Dependencies auto-fetch --> Audited dependencies

I migrated my MCP server to a remote architecture:

  1. Host the MCP server on a VPS
  2. Use HTTPS with authentication
  3. Dependencies are pinned and audited
  4. Server runs as unprivileged user
  5. 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:

What uvx does internally
# When you run: uvx my-mcp-server
uvx 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 server

The 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:

audit_mcp_deps.sh
#!/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/null
done
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!"
fi
done

Output:

Audit results
=== MCP Server Dependency Audit ===
Config: ~/.config/claude-code/mcp_config.json
context7: uvx
my-server: uvx
=== Checking for unpinned dependencies ===
Project: ~/.local/share/uv/tools/my-server
litellm>=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 restricted user for MCP
# Create a non-admin user
sudo sysadminctl -addUser mcp_runner -password "$(openssl rand -base64 32)" -admin no
# Set restricted home directory
sudo createhomedir -c -u mcp_runner
# Limit network access (macOS)
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --blockapp /Users/mcp_runner/.local/bin/uvx

Then run MCP servers as that user:

mcp_config.json - Restricted execution
{
"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:

Block network for uvx (macOS)
# Create a socket filter rule
sudo /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:

Sandbox with firejail
firejail --noprofile --net=none uvx my-mcp-server

The Complete Security Checklist

MCP 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 cache

Why This Matters

MCP servers are becoming standard for AI tools. Claude Code, Cursor, and others rely on them. But the current ecosystem has risks:

  1. Many MCP servers are unmaintained - Dependencies go stale
  2. No standard security model - Each server handles auth differently
  3. Auto-download is the default - uvx/npx fetch without asking
  4. 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.

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:

Recommended mcp_config.json
{
"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:

Audit commands
# Check MCP configurations
cat ~/.config/claude-code/mcp_config.json
cat ~/.cursor/mcp.json
# Check installed MCP tools
ls -la ~/.local/share/uv/tools/
# Audit dependencies
find ~/.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:

  1. Remote MCP is safer - Server runs on controlled infrastructure, not your machine
  2. Pin everything - Use exact versions and lock files with hashes
  3. Audit before using - Check MCP server code and dependencies
  4. Restrict permissions - Run as unprivileged user if local
  5. 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:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments