Dev.to AI 🤖 Ai 👁 0 📖 7 min read

Your MCP config is now an attack surface: a 20-minute inventory, and the four October incidents behind it

For most of this year, "MCP security" meant a hypothetical: what if a tool description tricks the model? October made it concrete. Four incidents in about two weeks all point at the same, unglamorous place: the list of s

For most of this year, "MCP security" meant a hypothetical: what if a tool description tricks the model? October made it concrete. Four incidents in about two weeks all point at the same, unglamorous place: the list of servers and keys your coding tools load when they start.

I'm not going to tell you to stop using MCP. I use it. What I'd do instead is spend 20 minutes making that list visible, then make it boring to change. Below are the four incidents (only what I could confirm in the source), then the inventory, step by step, with a small script.

The four incidents

1. A reverse shell registered as an MCP tool. Per CloudSEK's report (October 5, summarized by Platform Engineering on October 19), a ransomware affiliate registered a reverse shell as a tool inside an AI coding assistant through MCP and ran live attack operations through it. More than twenty organizations in six countries were compromised. The same operator scanned the internet for exposed MCP server ports. The article's key line for defenders: there is no CVE to patch here.

That's the uncomfortable part. Nothing was "broken." A tool was added, and the assistant used it as designed.

Source: https://platformengineering.com/features/attackers-have-found-mcp-what-platform-teams-should-do-about-it/

2. "Add MCP server" turned out to be a shell prompt. Langflow CVE-2026-105740 (CVSS 9.9): any authenticated user, not just an admin, could get code execution on the server by adding an MCP server with the stdio transport. The command went to bash -c without validation. Versions before 1.9.0 are affected. The Hacker News reports honeypots logged over 1,700 exploit attempts. A second bug in the same area, CVE-2026-105697, was patched in 1.10.3.

Source: https://thehackernews.com/2026/10/langflows-critical-authenticated-rce.html

3. An infostealer that now reads AI tool configs. Symantec reported on October 15 that the newest version of the Aoki infostealer targets .claude.json, Claude desktop cookies, the Codex config.toml and .env files, plus tokens for GitHub Copilot, Cursor, Gemini, JetBrains AI Assistant and Kiro, and AWS credentials. Older samples didn't look for AI-related files at all. The AI targeting is new.

Source: https://www.security.com/threat-intelligence/aoki-infostealer-ai-targeting

4. Keys stored in someone else's dashboard. The Hacker News reported on October 19 that a breach of a legacy OpenCode Console system may have exposed provider API keys for about 1.4% of Console users. No MCP involved, but the same lesson: a key you paste into a third-party service leaks on their schedule, not yours.

Source: https://thehackernews.com/2026/10/opencode-console-breach-may-have.html

Put together: the config files that list your MCP servers, and the keys sitting next to them, are now worth stealing and worth tampering with. So the first job is knowing what's in them.

The 20-minute inventory

Step 1 (5 min): find every file that can start a server

On a typical solo setup, MCP servers can be defined in several places at once:

  • ~/.claude.json (Claude Code, user-level and per-project mcpServers)

  • .mcp.json in a repo (Claude Code, project-scoped, and it travels with the repo)

  • ~/.cursor/mcp.json (Cursor)

  • ~/.codex/config.toml (Codex CLI, [mcp_servers.*] tables)

  • claude_desktop_config.json (Claude Desktop)

The .mcp.json in a repo is the one people forget. If you clone a project that ships one, its servers are on your list too, subject to whatever approval prompt your tool shows.

Step 2 (5 min): write down each server, its owner and what it can reach

For every server, one line: name, command or URL, who maintains it, and what it can touch (files, network, which credentials in its env). If you can't name the maintainer, that's a finding. If its env block holds a long-lived token with write scope, that's a finding.

Here is the script I use for the listing. Standard library only. It prints server names and commands, never env values or file contents, and it stores SHA-256 hashes so the next run tells you which files changed.

!/usr/bin/env python3

"""List MCP servers your coding tools will start; notice when key files change.
python3 mcp_inventory.py # print inventory, compare with baseline
python3 mcp_inventory.py --save # store current hashes as the baseline
"""
import hashlib, json, pathlib, sys

HOME = pathlib.Path.home()
CANDIDATES = [
HOME / ".claude.json",
pathlib.Path.cwd() / ".mcp.json",
HOME / ".cursor" / "mcp.json",
HOME / ".codex" / "config.toml",
HOME / "Library/Application Support/Claude/claude_desktop_config.json",
HOME / "AppData/Roaming/Claude/claude_desktop_config.json",
pathlib.Path.cwd() / ".env",
]
BASELINE = HOME / ".mcp_inventory_baseline.json"

def servers_in(p):
if p.suffix == ".toml":
for line in p.read_text(encoding="utf-8", errors="replace").splitlines():
s = line.strip()
if s.startswith("[mcp_servers.") and s.endswith("]"):
yield ("codex", s[len("[mcp_servers."):-1], "?")
return
if p.suffix != ".json":
return
try:
data = json.loads(p.read_text(encoding="utf-8"))
except (ValueError, UnicodeDecodeError):
yield ("?", "(unreadable JSON)", ""); return
blocks = [("user", data.get("mcpServers") or {})]
for proj, cfg in (data.get("projects") or {}).items():
if isinstance(cfg, dict):
blocks.append((proj, cfg.get("mcpServers") or {}))
for scope, block in blocks:
for name, cfg in block.items():
cfg = cfg if isinstance(cfg, dict) else {}
cmd = cfg.get("command") or cfg.get("url") or cfg.get("type") or "?"
args = [str(a) for a in cfg.get("args", [])]
yield (scope, name, " ".join([str(cmd)] + args)[:120])

old = json.loads(BASELINE.read_text()) if BASELINE.exists() else {}
now = {}
for p in CANDIDATES:
if not p.is_file():
continue
h = hashlib.sha256(p.read_bytes()).hexdigest()
now[str(p)] = h
flag = "" if not old else (" [same]" if old.get(str(p)) == h else " [CHANGED or new]")
print(f"{p}{flag}")
for scope, name, cmd in servers_in(p):
print(f" {scope:<12} {name:<24} {cmd}")
for gone in sorted(set(old) - set(now)):
print(f"{gone} [missing since baseline]")
if "--save" in sys.argv:
BASELINE.write_text(json.dumps(now, indent=2))
print(f"baseline saved: {BASELINE}")
elif not old:
print("no baseline yet: run again with --save")One honest caveat: ~/.claude.json changes often on its own, because Claude Code writes session state into it. A [CHANGED] flag on that file is normal. What matters is whether the server list under it changed. The hash is more useful for .mcp.json, mcp.json, config.toml and .env, which shouldn't move unless you moved them.

I tested it against a throwaway home directory with fake configs before publishing: it listed all four servers, flagged only the file I edited after --save, and never printed the dummy token sitting in an env block.

Step 3 (5 min): cut

For each server, ask: did I use this in the last two weeks? If not, remove it. A server you forgot about still starts with your agent and your credentials. Then, for the ones you keep:

  • Disable tools you don't use (many servers ship with write tools on by default).

  • Move long-lived tokens out of env blocks in shared files. At minimum, never commit them in a repo's .mcp.json.

  • Anything listening on a network port: bind to localhost or put it behind auth. The CloudSEK operator was scanning for exposed MCP ports.

Step 4 (5 min): make leaks cheap

You can't fully prevent key theft on a laptop that runs an infostealer. You can make a stolen key boring:

  • One key per tool. If your coding agent, your MCP GitHub server and your CI share one token, one leak means rotating everything and guessing what was used where.

  • A spend cap set at the provider, not in your code. Code-side limits don't help when the key is used from someone else's machine.

  • Write down the revoke steps for each key now, while you're calm: which console, which page.

What this looks like in my own setup

I run a small, mostly unattended publishing stack (scheduled jobs that post through the X API and record results). It's not an enterprise environment, but the same rules apply:

  • The X API key sits in a .env outside any repo, and the billing cycle cap is set in the X developer console ($5), with auto-recharge off. If the key leaked, the damage is bounded by that cap, not by my code.

  • The posting script checks which account it is authenticated as before every post and stops if it's not the expected one.

  • It writes attempt to a ledger before sending and sent after. So far that ledger shows 29 attempts and 29 sends, no unknowns, no duplicates.

And one thing I haven't fixed yet: the hooks in my own Claude Code guardrails plugin still don't set onFailure, so if Python is missing the guard is skipped rather than blocking. I wrote about that last week. It's still on the list. Inventories are useful partly because they show you your own gaps.

The checklist, short version

  • List every file that can define an MCP server (including repo .mcp.json).

  • One line per server: command, maintainer, what it can reach, which credentials.

  • Hash the config and .env files; re-run weekly and read what changed.

  • Remove anything unused for two weeks; disable unused tools.

  • No secrets in committed MCP configs; nothing listening on a public port.

  • One key per tool, provider-side spend caps, revoke steps written down.

If you want this as a printable one-pager with the rest of the pre-flight checks I run before letting an agent act on its own, the free checklist is here: https://neuralalpha.gumroad.com/l/agent-ops-preflight?utm_source=substack&utm_medium=article

Originally published on NeuralAlpha. I write weekly about running AI agents in production.

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.