The LiteLLM supply chain attack last week was a brutal wake-up call. Audit your dependencies. Check what you install. Stop trusting blindly. The hackers now carry heat-seeking precision weapons and you're running around with a cardboard shield.
After the audit on my main project, I opened my secondary MCP servers. The ones I built myself, on a random Tuesday, because I needed them and didn't want to think about auth at that moment. I found 7 critical vulnerabilities. All spec-compliant. None would have triggered a warning, a lint, an audit. Because the protocol says it's fine.
LiteLLM was someone poisoning someone else's code. My MCP setup was a supply chain attack I inflicted on myself. And if you're running MCP in production, there's a decent chance you did the same thing.
TLDR: I audited my own MCP servers after the LiteLLM incident. 7 critical flaws, all spec-compliant. The problem isn't developer negligence, it's a protocol that made the insecure path easier than the secure one. One afternoon of fixes, $0 extra infrastructure, and probably saved my production data from walking out the door. Go open your mcp.json.
The Mistake of Auditing My Own MCP Setup
Last week I wrote about the LiteLLM supply chain attack and 8 months of unsupervised pip installs. Everyone nodded. "Yes, we should audit our dependencies." "Yes, supply chain attacks are scary." "Yes, someone should do something." Consensus is comfortable like that.
Then I made the mistake of actually following my own advice.
Not on dependencies this time. On my own MCP servers. The ones I wrote, deployed, and connected to Claude Code because I needed them running and I figured: my code, my infra, my tools. How bad can it be?
Seven critical vulnerabilities bad.
MCP Has a Trust Problem by Design
I didn't ship those 7 vulnerabilities because I'm sloppy. I shipped them because MCP's spec made it the rational thing to do on a Tuesday afternoon when my kids were screaming about pool time.
The protocol was designed for one thing: get tools talking to each other as fast as possible. Security got the "implementation detail" treatment, which in protocol design means "nobody will do it."
No native identity. A token is a token. Your Claude Desktop and some script a stranger wrote both look the same to an MCP server. The protocol has no mechanism to tell them apart, so it doesn't try.
No least-privilege enforcement. You connect a tool, it gets everything. Granular scoping? Build it yourself. The spec doesn't even sketch what scopes should look like. So you'll do what I did: skip it.
No audit trail. Something goes wrong, you're grep-ing nginx logs at 11pm hoping you find something useful. The protocol doesn't define what "logging a tool call" means, so most servers don't.
The dev community knows this is broken. The "RIP MCP" debate has been loud on X, and for good reason. The frustration isn't about bugs. It's about a trust model that was never there to begin with.
The spec is improving. v2025-06-18 added auth elements. But thousands of servers in production were built on the permissive defaults, and the spec doesn't impose retroactive remediation. The spec got better. The installed base didn't.
Findings That Are the Spec's Fault (And Mine)
I'll own each one before I explain it. What I did first, then why the spec let me.
Finding 1: The 100-year JWT token
const token = jwt.sign(
{ serverId: 'content-api' },
SECRET,
{ expiresIn: '100y' }
);
I didn't want to deal with token refresh logic. So I wrote expiresIn: '100y' and moved on with my life. That's not a token. That's a permanent password wearing a JWT costume. The MCP spec doesn't mention maximum token lifetimes. Token expiration is "an implementation detail." So I implemented: never.
Finding 2: Open OAuth registration
I was the only user. Why build a whitelist for a party of one?
The spec doesn't require a pre-approved client list. So any script could register as an OAuth client and get complete access. Registration stayed open because closing it felt like overkill, and overkill on a Tuesday afternoon between two deploys doesn't happen.
Finding 3: Zero scoping
One token, all tools. Read, write, admin. Same key opens every door. MCP doesn't define a permissions model, so I didn't build one. My access control had exactly one level: everything.
Finding 4: No rate limiting
No throttle, no circuit breaker. A compromised token could exfiltrate the entire dataset by blasting requests at full speed. Nobody's counting because the protocol doesn't say anyone should count.
The remaining three (bearer tokens stored in plaintext in ~/.claude/mcp.json, the same token copy-pasted across 3 config files, a GitHub PAT sitting in settings.json) are documented in the article where I found my own secrets sprawled across config files after a simple folder move. Short version: plain text credentials in your home directory, duplicated everywhere, no encryption.
Seven findings. All spec-compliant. Here's how they chain together.
The Attack Chain Nobody Talks About
What bothers me is not the individual findings. It's the cascade.
Step 1. Access the config file. ~/.claude/mcp.json. Home directory. Not hidden, not encrypted, not protected by anything you probably ever thought about.
Step 2. Read the token. It's plain text. That's it. That's the exploit.
Step 3. Call MCP endpoints. List every tool, read every piece of content, access every resource. No scope restrictions because there aren't any.
Step 4. Hit the HTTP API. Same tokens work because you wouldn't maintain two separate auth systems for the same data (admit it, you wouldn't either).
Step 5. Production is compromised. Content, data, configuration. Everything.
Total elapsed time: minutes.
Here's what should bother you more than LiteLLM: none of this requires a supply chain attack. Nobody needs to compromise a package on PyPI. Nobody needs to inject malicious code. The whole attack surface is a config file in your home directory that you haven't opened since the day you generated it.
The Mirror Test
Why did I do all of this?
I'm sitting in my apartment in Panama, the air conditioning barely winning against the humidity, and I'm staring at a JWT token I gave a 100-year lifespan. I wrote 4 security articles in 8 days. I know this stuff. And I still did the easy thing every single time because the protocol let me.
Deedy Das had the most honest take I've read on this: the fundamental premise of vibecoding is the premise of a supply chain attack. You run code you didn't read, from sources you didn't verify, with permissions you didn't scope. That's not a workflow, that's a threat model.
The numbers confirm it's not just me being an idiot. Security researchers found roughly 7,000 MCP servers exposed on the open internet. About half had no authorization controls at all. CVE-2025-6514 hit CVSS 9.6 for command injection through basic input manipulation. These aren't edge cases. These are the defaults.
And this is where I think the whole "developers need to be more careful" argument falls apart. I was careful. I literally spent the previous week writing about supply chain attacks. Didn't matter. The protocol's path of least resistance led me straight into every hole, because designing a secure setup meant building everything the spec left out, and building everything the spec left out meant not shipping that afternoon.
There's a reason CLIs don't have this problem when you use them as AI agent tooling. No abstraction layer hiding the attack surface. You see what runs. You control what's exposed. The security model is right there in the command, not buried somewhere in a spec nobody reads.
The Afternoon Fix
One afternoon. Here's what changed, and I'm almost annoyed at how little work it was.
Token rotation was the first thing. Every service gets its own token now. Content API, analytics, deployment. Separate tokens, separate revocation. I should have done this from day one, but "one token for everything" felt efficient back when I thought efficient and secure were the same thing (they're not, and I knew that, and I did it anyway).
Short-lived tokens took 15 lines of code. I borrowed the pattern from how Convex handles auth: generate, use, expire in 5 minutes. I spent more time overthinking the approach than actually writing the token refresh. The old token had a 100-year lifespan. The new one lives shorter than my morning coffee stays warm.
Scoping was the part I'd been dreading for no reason. Three levels: read-only for dashboards, read-write for content operations, admin for deployment. Each token knows what it can touch. Took maybe 40 minutes including testing.
Rate limiting: 10 requests per minute for writes, 60 for reads. A compromised token now has a speed limit.
Killed the open registration. Pre-approved client list only. Not on the list, no token.
Total: $0 in additional infrastructure. Same servers, same setup. The fix was never expensive. The cost was assuming "my code, my infra" meant "safe by default."
Looking back at this, the 100-year token would never have existed if I'd applied prompt contracts from the start. When you spec your security constraints before writing code, "I'll deal with auth later" stops being a valid option. The spec catches it before the code ships.
Defaults beat intentions every time.
Your Config File Is Right There
Your mcp.json is sitting in your home directory right now. When's the last time you opened it?
Now check the other ones.
Sources
SC Media: MCP security analysis (exposed servers, CVE-2025-6514)
(*) The cover is AI-generated. The tokens in the image are more secure than the ones I found in my config files.

Top comments (0)