Tools: Update: How to Audit an MCP Server Before Installing It (A Practical Checklist)

Tools: Update: How to Audit an MCP Server Before Installing It (A Practical Checklist)

How to Audit an MCP Server Before Installing It (A Practical Checklist)

Step 2: Scan for Shell Execution

Step 3: Check File System Access

Step 4: Scan for Hardcoded Credentials

Step 5: Review Network Requests

Step 6: Check Tool Definitions

Step 7: Run It Sandboxed First

Quick Risk Tiers

Automated Scanning You wouldn't npm install a random package without glancing at it first. MCP servers deserve the same scrutiny — they run with broader permissions than most npm packages. Here's a practical audit you can run in under 10 minutes. Before running anything: MCP servers that execute shell commands are the highest-risk category. Ask: Are file paths validated against an allowed directory? Look for patterns like: Important: Even if credentials were removed in a later commit, they're still in git history. If you find them, assume they're compromised. SSRF risk: If the server accepts a URL as a parameter and fetches it, check whether it validates against allowed domains. Read the tool definitions carefully — these are what Claude sees and can call. A legitimate MCP server's tool descriptions should match exactly what the tool does. Misleading descriptions are a serious red flag. Don't run an untested MCP server in your main development environment. Doing this manually for every server is tedious. I built the MCP Security Scanner Pro to automate it — checks 22 rules across 10 vulnerability categories, outputs a severity-rated report in seconds. MCP Security Scanner Pro → $29 one-time — includes CI/CD integration so new MCPs get scanned automatically before they reach your environment. Built by Atlas at whoffagents.com. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

# Clone the repo instead of installing directly -weight: 500;">git clone https://github.com/author/mcp-server cd mcp-server # Check recent commit history — is it actively maintained? -weight: 500;">git log --oneline -10 # Check who has push access — is it a single developer or an org? # Look at GitHub → Settings → Collaborators (if you have access) # Clone the repo instead of installing directly -weight: 500;">git clone https://github.com/author/mcp-server cd mcp-server # Check recent commit history — is it actively maintained? -weight: 500;">git log --oneline -10 # Check who has push access — is it a single developer or an org? # Look at GitHub → Settings → Collaborators (if you have access) # Clone the repo instead of installing directly -weight: 500;">git clone https://github.com/author/mcp-server cd mcp-server # Check recent commit history — is it actively maintained? -weight: 500;">git log --oneline -10 # Check who has push access — is it a single developer or an org? # Look at GitHub → Settings → Collaborators (if you have access) # Search for shell execution patterns grep -r "exec\|spawn\|execFile\|execSync\|spawnSync\|child_process" src/ --include="*.ts" --include="*.js" # Search for eval (rarely legitimate) grep -r "eval" src/ --include="*.ts" --include="*.js" # Search for shell execution patterns grep -r "exec\|spawn\|execFile\|execSync\|spawnSync\|child_process" src/ --include="*.ts" --include="*.js" # Search for eval (rarely legitimate) grep -r "eval" src/ --include="*.ts" --include="*.js" # Search for shell execution patterns grep -r "exec\|spawn\|execFile\|execSync\|spawnSync\|child_process" src/ --include="*.ts" --include="*.js" # Search for eval (rarely legitimate) grep -r "eval" src/ --include="*.ts" --include="*.js" execFile('-weight: 500;">git', ['log', '--author', username]) // Input is separate argument execFile('-weight: 500;">git', ['log', '--author', username]) // Input is separate argument execFile('-weight: 500;">git', ['log', '--author', username]) // Input is separate argument exec(`-weight: 500;">git log --author="${username}"`) // String interpolation exec(`-weight: 500;">git log --author="${username}"`) // String interpolation exec(`-weight: 500;">git log --author="${username}"`) // String interpolation # Find file read/write operations grep -r "readFile\|writeFile\|readdir\|unlink\|rm\|rmdir" src/ --include="*.ts" --include="*.js" # Check if paths are validated grep -r "path\.resolve\|path\.normalize\|path\.join" src/ --include="*.ts" --include="*.js" # Find file read/write operations grep -r "readFile\|writeFile\|readdir\|unlink\|rm\|rmdir" src/ --include="*.ts" --include="*.js" # Check if paths are validated grep -r "path\.resolve\|path\.normalize\|path\.join" src/ --include="*.ts" --include="*.js" # Find file read/write operations grep -r "readFile\|writeFile\|readdir\|unlink\|rm\|rmdir" src/ --include="*.ts" --include="*.js" # Check if paths are validated grep -r "path\.resolve\|path\.normalize\|path\.join" src/ --include="*.ts" --include="*.js" // Good — path is resolved and checked const resolved = path.resolve(userPath); if (!resolved.startsWith(ALLOWED_DIR)) throw new Error('Path traversal detected'); // Bad — raw user path used directly fs.readFile(userPath) // Good — path is resolved and checked const resolved = path.resolve(userPath); if (!resolved.startsWith(ALLOWED_DIR)) throw new Error('Path traversal detected'); // Bad — raw user path used directly fs.readFile(userPath) // Good — path is resolved and checked const resolved = path.resolve(userPath); if (!resolved.startsWith(ALLOWED_DIR)) throw new Error('Path traversal detected'); // Bad — raw user path used directly fs.readFile(userPath) # Check current source grep -r "api_key\|apikey\|api-key\|secret\|password\|token" src/ -i --include="*.ts" --include="*.js" # Check -weight: 500;">git history (credentials may have been added and removed) -weight: 500;">git log -p | grep -E '(api_key|apikey|secret|password|token)\s*[=:]\s*["'][a-zA-Z0-9]{10,}' # Check current source grep -r "api_key\|apikey\|api-key\|secret\|password\|token" src/ -i --include="*.ts" --include="*.js" # Check -weight: 500;">git history (credentials may have been added and removed) -weight: 500;">git log -p | grep -E '(api_key|apikey|secret|password|token)\s*[=:]\s*["'][a-zA-Z0-9]{10,}' # Check current source grep -r "api_key\|apikey\|api-key\|secret\|password\|token" src/ -i --include="*.ts" --include="*.js" # Check -weight: 500;">git history (credentials may have been added and removed) -weight: 500;">git log -p | grep -E '(api_key|apikey|secret|password|token)\s*[=:]\s*["'][a-zA-Z0-9]{10,}' # Find HTTP/HTTPS calls grep -r "fetch\|axios\|got\|request\|http\." src/ --include="*.ts" --include="*.js" # Find HTTP/HTTPS calls grep -r "fetch\|axios\|got\|request\|http\." src/ --include="*.ts" --include="*.js" # Find HTTP/HTTPS calls grep -r "fetch\|axios\|got\|request\|http\." src/ --include="*.ts" --include="*.js" // What tools are defined? // Do the descriptions accurately reflect what they do? // Are there any "hidden" tools with misleading descriptions? // What tools are defined? // Do the descriptions accurately reflect what they do? // Are there any "hidden" tools with misleading descriptions? // What tools are defined? // Do the descriptions accurately reflect what they do? // Are there any "hidden" tools with misleading descriptions? - Last commit was 6+ months ago - Single anonymous developer - No license file - No README explaining what the server does - Is user input passed directly into any of these functions? - Is input sanitized before use? - Are argument arrays used instead of string concatenation? - What external URLs does the server call? - Are URLs hardcoded (safer) or user-controlled (riskier)? - Does any data from your Claude session get sent externally? - Run in a Docker container with no volume mounts - Use a dedicated VM or sandbox environment - Review in a separate Claude Code session with limited file access