MCP Server Source Code Security Linter — Zero-dependency static analysis for MCP (Model Context Protocol) server Python code.
43% of MCP server implementations contain command injection flaws. 30% permit unrestricted URL fetching. mcplint catches these issues before deployment.
Existing MCP security tools (mcpwn, mcp-scan, Cisco mcp-scanner) are runtime scanners — they connect to running MCP servers and probe them. mcplint is different: it statically analyzes your source code before you ship it.
Think of it as ESLint for MCP servers. Catch command injection, SQL injection, SSRF, path traversal, and tool description poisoning in your CI pipeline — not in production.
# Scan an MCP server file
python mcplint.py server.py
# Scan a directory
python mcplint.py src/
# CI mode (exit 1 if issues found)
python mcplint.py --check server.py
# JSON output
python mcplint.py --json server.py
# Only show high+ severity
python mcplint.py --severity high server.py
# Show fix suggestions
python mcplint.py --verbose server.py
# Read from stdin
cat server.py | python mcplint.py -mcplint detects 15 categories of security issues in MCP tool, resource, and prompt handlers:
| Rule | Severity | What It Detects |
|---|---|---|
| ML01 | 🔴 CRITICAL | Command Injection — User input passed to subprocess/os.system/os.popen with shell execution |
| ML02 | 🟠 HIGH | Path Traversal — User input used in file operations without sanitization |
| ML03 | 🔴 CRITICAL | SQL Injection — User input formatted into SQL queries (f-strings, .format(), %, concatenation) |
| ML04 | 🟠 HIGH | SSRF — User-controlled URLs in HTTP requests |
| ML05 | 🔴 CRITICAL | Code Execution — User input passed to eval/exec/compile |
| ML06 | 🔴 CRITICAL | Unsafe Deserialization — pickle/yaml.load/marshal in tool handlers |
| ML07 | 🔴 CRITICAL | Hardcoded Secrets — API keys, tokens, private keys in source code |
| ML08 | 🟡 MEDIUM | Missing Input Validation — Tool parameters without type annotations |
| ML09 | 🟠 HIGH | Sensitive File Access — References to .env, .ssh, credentials files |
| ML10 | 🟡 MEDIUM | Information Disclosure — Returning tracebacks, exception details, env vars |
| ML11 | 🟡 MEDIUM | Missing Error Handling — Risky operations without try/except |
| ML12 | 🔴 CRITICAL | Description Injection — Prompt injection patterns in tool docstrings |
| ML13 | 🟡 MEDIUM | Excessive Permissions — shell=True without user input, sudo commands |
| ML14 | 🟡 MEDIUM | Missing Timeout — HTTP/subprocess calls without timeout parameter |
| ML15 | 🔵 LOW | Resource Leaks — Files opened without context managers |
mcplint uses Python's ast module to understand your MCP server code:
- Finds MCP handlers — Detects
@mcp.tool(),@server.tool(),@mcp.resource(),@mcp.prompt()decorators (works with FastMCP, official SDK, and custom setups) - Identifies user input — Tool parameters come from LLMs/users and are treated as untrusted
- Traces data flow — Follows parameter usage through function bodies to find dangerous sinks
- Checks descriptions — Scans tool docstrings for prompt injection patterns
Given this vulnerable MCP server:
import subprocess
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Demo")
@mcp.tool()
def run_command(command: str) -> str:
"""Execute a shell command"""
return subprocess.check_output(command, shell=True).decode()
@mcp.tool()
def search_db(query: str) -> str:
"""Search the database"""
cursor.execute(f"SELECT * FROM data WHERE q = '{query}'")
return str(cursor.fetchall())mcplint output:
────────────────────────────────────────────────────────────
server.py
────────────────────────────────────────────────────────────
🔴 CRITICAL ML01 L9: Command injection: tool 'run_command' passes user input
to subprocess.check_output() with shell execution (tool: run_command)
🔴 CRITICAL ML03 L14: SQL injection: tool 'search_db' uses f-string with
user input in SQL query (tool: search_db)
════════════════════════════════════════════════════════════
mcplint — F (0/100)
2 finding(s): 🔴 2 critical
════════════════════════════════════════════════════════════
- name: Lint MCP Server
run: python mcplint.py --check --severity medium src/- repo: local
hooks:
- id: mcplint
name: mcplint
entry: python mcplint.py --check
language: python
files: '\.py$'| Flag | Description |
|---|---|
--check |
Exit 1 if any findings (for CI) |
--json |
JSON output |
--severity |
Minimum severity: info, low, medium, high, critical |
--ignore |
Comma-separated rules to skip: ML07,ML15 |
--verbose / -v |
Show fix suggestions |
--no-color |
Disable emoji output |
- MCP-aware: Only scans files with MCP decorators. Non-MCP Python files are silently skipped.
- User input tracking: Tool parameters are treated as untrusted. Static string arguments are not flagged as injection risks.
- Zero false positives on safe patterns: Parameterized SQL queries,
yaml.safe_load(), static URLs with user data in params — all pass clean. - Description analysis: The only static tool that checks for prompt injection in tool docstrings (a real MCP attack vector per Elastic Security Labs).
- Python 3.9+
- Zero dependencies
- Elastic Security Labs: MCP Tools Attack Vectors — 43% command injection, tool poisoning, orchestration injection
- Equixly: MCP Server Security Nightmare — 30% unrestricted URL fetching
- Snyk Labs: Prompt Injection Meets MCP — Tool description poisoning
- Notepad++ Supply Chain Attack (2026) — Why static analysis of tool code matters
MIT