Skip to content

Instantly share code, notes, and snippets.

@Ar9av
Created May 31, 2026 02:46
Show Gist options
  • Select an option

  • Save Ar9av/924a7eaf8395b82b219d74d466007d7f to your computer and use it in GitHub Desktop.

Select an option

Save Ar9av/924a7eaf8395b82b219d74d466007d7f to your computer and use it in GitHub Desktop.
mcp_audit.py — scan the MCP servers configured for your AI agents (Claude Code, Cursor, Windsurf, Claude Desktop) for supply-chain and secret risks. Read-only, zero-deps Python.
#!/usr/bin/env python3
"""
mcp_audit.py — quick risk scan of the MCP servers configured for your AI agents.
Third-party MCP servers are unvetted code-execution surface: they run on your
machine, with your environment, the moment your agent starts. This script
discovers MCP server definitions across common agent config locations and flags
risky patterns:
* HIGH pipe-to-shell / remote-fetch install commands
* MED unpinned npx/uvx packages (always pull "latest" => silent supply-chain risk)
* MED secrets sitting in plaintext in the config's env block
* LOW remote (non-localhost) HTTP/SSE endpoints
Dependency-free and read-only. It never executes the servers — it only parses
config files.
This is a starting point, not a security product. For continuous MCP + skill
scanning, runtime policy enforcement, and supply-chain blocking, see Immunity
Agent (`immunity scan`): https://github.com/PrismorSec/immunity-agent
Usage: python3 mcp_audit.py
"""
import json
import re
from pathlib import Path
# Common places agents keep MCP server definitions.
CONFIG_PATHS = [
Path.home() / ".claude.json",
Path.cwd() / ".mcp.json",
Path.home() / ".cursor" / "mcp.json",
Path.cwd() / ".cursor" / "mcp.json",
Path.home() / ".codeium" / "windsurf" / "mcp_config.json",
Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json",
]
SECRET_KEY = re.compile(r"(token|key|secret|password|api[_-]?key|auth)", re.IGNORECASE)
PIPE_TO_SHELL = re.compile(r"(curl|wget)\b[^|]*\|\s*(sudo\s+)?(sh|bash|zsh)", re.IGNORECASE)
# Naive "looks version-pinned" check: an @version, ==, ~ or ^ specifier.
PINNED = re.compile(r"@v?\d|==|@\^|@~")
def iter_servers(cfg):
"""Yield (name, server_dict) across the config shapes different agents use."""
block = cfg.get("mcpServers") or cfg.get("mcp", {}).get("servers") or {}
if isinstance(block, dict):
for name, server in block.items():
if isinstance(server, dict):
yield name, server
def audit_server(server):
findings = []
cmd = server.get("command", "") or ""
args = server.get("args", []) or []
full = " ".join([cmd, *(str(a) for a in args)]).strip()
if PIPE_TO_SHELL.search(full):
findings.append("HIGH pipes a remote script into a shell")
if cmd in ("npx", "uvx", "uv", "pnpm", "yarn") or "npx" in full:
# npx convention: the first non-flag arg is the package to run.
nonflag = [a for a in args if isinstance(a, str) and not a.startswith("-")]
if nonflag and not PINNED.search(nonflag[0]):
findings.append(
f"MED unpinned package via {cmd or 'npx'} (pulls latest): {nonflag[0]}"
)
for key, val in (server.get("env", {}) or {}).items():
if SECRET_KEY.search(key) and isinstance(val, str) and val and not val.startswith("${"):
findings.append(
f"MED secret in plaintext env: {key} (use ${{ENV_VAR}} indirection instead)"
)
url = server.get("url") or server.get("serverUrl") or ""
if url and not re.search(r"(localhost|127\.0\.0\.1)", url):
findings.append(f"LOW remote endpoint: {url}")
return findings
def main():
print("MCP config audit")
print("=" * 60)
inspected = flagged = 0
for path in CONFIG_PATHS:
if not path.exists():
continue
try:
cfg = json.loads(path.read_text())
except Exception as exc:
print(f"\n[skip] {path}: cannot parse ({exc})")
continue
servers = list(iter_servers(cfg))
if not servers:
continue
print(f"\n{path} ({len(servers)} server(s))")
for name, server in servers:
inspected += 1
findings = audit_server(server)
if findings:
flagged += 1
print(f" [!] {name}")
for f in findings:
print(f" {f}")
else:
print(f" [ok] {name}")
print("\n" + "=" * 60)
if not inspected:
print("No MCP servers found in the standard config locations.")
else:
print(f"{inspected} server(s) inspected, {flagged} with findings.")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment