What it detects
Offline taint scanner for Python MCP server code. It finds where a tool's arguments end up.
An MCP tool's arguments are chosen by the model, and the model can be steered by anything it has read: a web page, an issue, a poisoned document. So every @mcp.tool() parameter is attacker-controlled input. mcpsink finds the tool, resource and call_tool handlers in a FastMCP or official-SDK server, treats their parameters as tainted, follows them through assignments and same-file helper functions, and reports when they reach a dangerous sink: a shell, a file path, an outbound URL, SQL, eval/pickle, or os.environ. It also flags HTTP transports bound to 0.0.0.0 and tool annotations (readOnlyHint, destructiveHint) that contradict what the code does.
Other agentsec tools look at what the server *says* (toolpoison: descriptions; trifecta: capability mix; mcpposture: client configs). mcpsink reads the server's *implementation*.
Standard library only (ast). Nothing is imported or executed. No network.
Rules
| Rule | Default severity | Finding |
|---|---|---|
| MS01 | critical (shell) / high (program) / low (argv) | Tool argument reaches os.system, os.popen, subprocess.*(shell=True), create_subprocess_shell (critical); chooses the program, or is passed as code to sh/python/node -c (high / critical); sits in an argv list without a preceding -- (low: option injection such as git --upload-pack) |
| MS02 | high | Tool argument used as a path for open, os.remove, shutil.*, os.listdir, Path(...).read_text/write_text/unlink/... without a containment check. With resolve()/realpath + is_relative_to/commonpath/startswith in the handler it drops to info |
| MS03 | high | Tool argument used as the URL/host for requests, httpx, aiohttp, urllib.request, http.client, socket or a client/session object (SSRF to 169.254.169.254/localhost, exfiltration via URL). A urlparse(...).hostname check drops it to info |
| MS04 | high | Tool argument in the SQL text of .execute()/executemany/executescript/text()/read_sql: f-string/%/+/.format built queries, or a tool that runs caller-supplied SQL |
| MS05 | critical | Tool argument reaches eval, exec, compile, __import__/importlib.import_module, pickle/dill/marshal loads, yaml.load without SafeLoader, torch.load, jinja2.Template, runpy |
| MS06 | high | os.getenv(arg) / os.environ[arg] with a tool-chosen name, or a tool that returns the whole os.environ |
| MS07 | medium | Server bound to all interfaces: host="0.0.0.0" / "::" in FastMCP(...), run(...), uvicorn.run(...), or *.host = "0.0.0.0" |
| MS08 | high / medium / low | readOnlyHint=True on a tool that writes/deletes files (high) or runs processes (medium); destructiveHint=False on a tool that writes/deletes (medium); writes, deletes or runs processes with no annotation at all (low) |
| MS09 | info | Inventory of handlers and the parameters treated as tainted |
Usage
cd tools/mcpsink
python3 mcpsink.py examples/risky # 2 servers, 13 handlers, 21 findings, exit 1
python3 mcpsink.py examples/clean --fail-on any # 9 handlers, 0 findings, exit 0
python3 mcpsink.py src/ --min-severity info # include guarded sinks and the handler inventory
python3 mcpsink.py server.py --sarif > mcpsink.sarif
python3 mcpsink.py . --decorator 'register_tool' --jsonAfter pip install of the kit, the same options work as agentsec mcpsink …. Every tool also accepts --help and almost every tool accepts --json.
Example output
See the examples under tools/mcpsink/examples/ in the repository.
SARIF and CI
agentsec mcpsink path/to/input --sarif > mcpsink.sarif
# GitHub Actions (composite action ships with the repo)
- uses: dunkleschitz/agentsec@v0
with:
tools: mcpsink
paths: path/to/input
fail-on: high
SARIF 2.1.0 can be uploaded to GitHub code scanning. The composite action writes per-tool reports under agentsec-sarif/ by default.
OWASP mapping
Heuristic mapping of what this tool helps a reviewer look for. Not an official OWASP endorsement, and not complete coverage of any item. Sources: OWASP Top 10 for LLM Applications 2025 and OWASP Top 10 for Agentic Applications 2026.
- LLM05 Improper Output Handling — OWASP LLM Top 10 (2025)
- ASI05 Unexpected Code Execution (RCE) — OWASP Agentic Top 10 (2026)
Known limits
- Python only. TypeScript/JavaScript MCP servers are not analysed.
- Intraprocedural plus same-file helpers. Taint is followed through assignments, loops,
with,
comprehensions and calls to functions or methods defined in the same file (up to 3 levels). A helper's return value counts as tainted when any argument was. Taint is not followed across modules, through class attributes or globals, or through framework magic (a Pydantic model parameter is one tainted value).
- Flow-insensitive. A variable is tainted if *any* assignment to it is tainted, and guards are detected
per function by pattern, not proven to dominate the sink. An is_relative_to check anywhere in the handler downgrades every path finding in it. That is why guarded findings are info, not silent.
- Sinks and sanitizers are lists. Unusual libraries (custom HTTP wrappers, ORMs,
sh,plumbum) are not
recognised; a custom sanitizer the tool doesn't know about produces a false positive (use the inline ignore).
- Annotation checks only see literal
annotations={...}/ToolAnnotations(...)values on the decorator. - A clean report means "no known pattern matched", not "safe". Use it as a review aid and CI tripwire.
Full source, tests, and fixtures: tools/mcpsink/ in the repository. Install the kit with pip install git+https://github.com/dunkleschitz/agentsec.git.