mcp-triage
An MCP security scanner. Audit an MCP server before you let an agent near it.
mcp-triage is a command-line security scanner for MCP (Model Context Protocol)
servers. It scans a server for prompt injection in tool descriptions, hardcoded
secrets, dangerous code execution paths, and unsafe defaults — locally, with no
account and no network calls.
Connecting an agent to a Model Context Protocol server hands that server two things at once: code execution on your machine, and a direct line to the model's context. The second one is what makes MCP different from an ordinary dependency.
A tool's description field isn't documentation for humans — it's fed
straight to the LLM as instructions. A server author (or someone who
compromised one) can write a description that tells your agent to do something
you never asked for, and you'd never see it in a code review that only looked
at the implementation. That's a new class of supply-chain risk, and there are
now 20,000+ MCP servers in public directories with almost no
security review between them and your agent.
mcp-triage is a fast, local, dependency-light first pass over any MCP
server — one you didn't write, or one you're about to publish.
Quick start
Audit a server you're thinking about trusting:
pip install mcp-triage
git clone https://github.com/some-org/some-mcp-server
mcp-triage scan ./some-mcp-server
That's the whole workflow. No account, no config file, no network calls — see Runs entirely on your machine.
Use the latest security rules and community improvements
Version 0.5.0 includes local MCP configuration auditing (MCP201 and MCP303–MCP306), new server-analysis rules (MCP006, MCP007, and MCP104), baseline scanning, and Markdown reports:
mcp-triage audit-local
mcp-triage scan ./some-mcp-server --write-baseline .mcp-triage-baseline.json
mcp-triage scan ./some-mcp-server --baseline .mcp-triage-baseline.json --format markdown
Install or upgrade with python -m pip install --upgrade mcp-triage. The
GitHub Action examples below use @main to access the latest repository code.
Audit MCP servers already configured on your machine
mcp-triage audit-local checks supported Claude Desktop/Code, Cursor, VS Code,
Windsurf, and Gemini configuration locations, along with MCP config files in
the current project:
mcp-triage audit-local
mcp-triage audit-local --format markdown
mcp-triage audit-local --path ./mcp.json --fail-on medium
The command is read-only and never prints credential values. It lists the
configuration files it checked and every configured server; use repeatable
--path options to audit specific files instead of discovery. Reports identify
potential credential values (redacted), unpinned npx/uvx packages,
download-and-shell launch commands, non-local plain HTTP endpoints, and sudo
or whole-root/home directory access. Docker image tags are not checked. A clean
result is not a guarantee that a configured server is safe.
What it catches
- 🧠 Prompt-injection-prone tool descriptions — phrasing aimed at the calling model rather than at a human reader ("ignore previous instructions", "do not tell the user"), hidden zero-width unicode, and descriptions long enough to bury instructions in. Checked in static JSON manifests and in the source-embedded string literals where real servers actually keep them.
- 🗂️ Prompt and resource metadata in JSON protocol manifests — checks descriptions for concealed access and sensitive-data exfiltration patterns.
- 🧩 Nested tool input schemas — finds explicitly open object schemas inside nested properties and composition keywords, not only at the root.
- 🔓 Over-broad capabilities — tools advertising shell/exec/arbitrary file
access, or accepting unvalidated free-form input (
additionalProperties: true). - 💣 Dangerous code paths in the implementation —
eval,subprocess(..., shell=True)(including imported aliases in Python),os.system,pickle.loads, unsafeyaml.load. - 🔑 Hardcoded secrets — API keys, AWS keys, GitHub/Slack tokens committed into source or config.
- 🌐 Unsafe defaults — binding to
0.0.0.0,trust/skip_authflags left on. - 🖥️ Risky local MCP configuration —
mcp-triage audit-localflags embedded credentials, unpinned package launches, remote download-to-shell commands, non-local plain HTTP, and elevated or broad filesystem access.
Full rule reference below. Every rule ID is stable — see the rule stability policy before you pin one in CI.
Use it in CI
GitHub Action — no pip install boilerplate:
- uses: YashkantG/mcp-triage@main
with:
path: ./my-mcp-server
fail-on: high
SARIF → GitHub Code Scanning, so findings land in the Security tab instead of scrolling past in a build log:
- uses: YashkantG/mcp-triage@main
with:
path: ./my-mcp-server
format: sarif
upload-sarif: "true"
To adopt the scanner in a repository with existing findings, generate a baseline once, check that JSON file into the repository, and pass it on later scans:
- uses: YashkantG/mcp-triage@main
with:
path: ./my-mcp-server
baseline: .mcp-triage-baseline.json
format: sarif
upload-sarif: "true"
Generate or intentionally refresh the baseline with
write-baseline: .mcp-triage-baseline.json. Baseline generation does not fail
the step for existing findings. The baseline scan then fails only when it finds
a new issue at or above the configured fail-on severity.
pre-commit:
repos:
- repo: https://github.com/YashkantG/mcp-triage
rev: v0.5.0
hooks:
- id: mcp-triage
Any other CI — mcp-triage scan . --fail-on high exits non-zero when it
finds something at or above that severity. --format json for machine-readable
output.
Adopt it without blocking on old findings
For a project adding security checks to an existing codebase, save the current findings once and fail future scans only when they introduce a new finding:
mcp-triage scan . --write-baseline .mcp-triage-baseline.json
mcp-triage scan . --baseline .mcp-triage-baseline.json --format sarif
Check the baseline file into version control and refresh it intentionally after reviewing existing findings. Baselines use a versioned JSON format and store finding fingerprints only (not code snippets); line-number shifts alone do not make an otherwise unchanged finding new. Invalid or unsupported baseline files fail with an explicit error.
Shareable scan reports
Use --format markdown to generate a readable report for a pull request,
issue, or security review:
mcp-triage scan . --format markdown > security-report.md
JSON and SARIF remain available for automation and code-scanning integrations.
Show your posture
If you publish an MCP server, --format badge emits a
shields.io endpoint payload you can
commit and display, so the people evaluating your server can see it was
checked:
mcp-triage scan . --format badge > .github/badges/mcp-security.json
[](https://github.com/YashkantG/mcp-triage)
Grades are deliberately blunt: A clean, B/C medium findings, D/F high findings. The badge at the top of this README is this repo scanning itself, regenerated and verified on every CI run.
Tuning it
A pattern-based scanner will flag things you've already reviewed. Three levers, narrowest first:
Inline, on the offending line:
subprocess.run(cmd, shell=True) # mcp-triage: ignore[MCP102]
subprocess.run(cmd, shell=True) # mcp-triage: ignore ← all rules, this line
Project config — .mcptriage.toml at the scan root:
[ignore]
rules = ["MCP004"] # repo-wide
paths = ["tests/fixtures/**"]
[severity]
MCP003 = "LOW" # downgrade rather than silence
[[custom_rules]] # your own checks, no fork required
id = "CUSTOM001"
pattern = "InternalOnlyApi\\.execute"
message = "Internal-only API called from an MCP tool handler"
severity = "HIGH"
ignore.paths patterns are matched against paths relative to the scan root
(for example, tests/fixtures/**) as well as absolute paths. When --config
points to a separate config file, path patterns still use the target being
scanned as their root.
CLI, for one-offs: mcp-triage scan . --ignore-rule MCP004
This repo's own .mcptriage.toml is a worked example.
Runs entirely on your machine
mcp-triage makes zero network calls. It doesn't phone home, doesn't
upload your code, and has no telemetry. Runtime dependencies are typer,
rich, and tomli (Python < 3.11 only).
Releases publish through PyPI Trusted Publishing (OIDC — no long-lived tokens), so every release traces back to the GitHub Actions run that built it. If your security team needs to approve a new tool: read the source, then run it with no network access at all. It doesn't need any.
Reporting a vulnerability — in this tool, or one you found with it — see SECURITY.md.
What the MCP ecosystem actually looks like
research/ecosystem_scan.py surveys public MCP servers harvested from the
curated awesome-lists. Against a pinned release, over a seeded sample of
139 successfully scanned repositories (pool of 3,701):
| Repositories with at least one finding | 35% |
| Long tool descriptions (MCP003) | 25 repos |
| Shell/exec sinks (MCP102 / MCP101) | 17 and 12 repos |
| Over-broad capability (MCP004) | 14 repos |
| Hardcoded secrets (MCP201) | 13 repos |
Treat these as provisional, and as a floor. The survey's real value so far has been finding bugs in this scanner, not in other people's servers. In order:
- The first pass reported 37% and 931 HIGH findings. Inspection showed most
were fake credentials in test fixtures,
evalin benchmark harnesses, andpattern.exec(line)— the JavaScript RegExp API — which alone was 77% of all code-execution hits. - The harness itself recorded crashed scans as "0 findings", quietly turning scanner failures into clean bills of health.
- Re-running against the pinned v0.4.0 produced 32 prompt-injection hits which, on inspection, were essentially all false positives — including descriptions being more careful than average about user consent. Fixed in v0.4.1.
- That same check revealed the extractor never matched
"description":(the quoted-key form used by Python dicts), so tools defined in dict literals were never scanned at all. Description-rule coverage in the numbers above is therefore understated.
Each of those became a precision regression test rather than a headline. No repository is named here; if this tool finds something real in someone else's server, disclose it to them privately.
Reproduce it yourself: python research/ecosystem_scan.py --sample-size 150.
Rules
| ID | Check |
|---|---|
| MCP001 | Prompt injection / tool poisoning in tool description |
| MCP002 | Hidden/invisible unicode characters in tool description |
| MCP003 | Suspiciously long tool description (payload smuggling risk) |
| MCP004 | Over-broad capability exposed by tool name/description |
| MCP005 | Tool schema accepts arbitrary/unvalidated input |
| MCP006 | Concealed or exfiltrative prompt/resource description |
| MCP007 | Nested tool schema accepts arbitrary fields |
| MCP101 | Dangerous code execution sink (eval, exec, new Function) |
| MCP102 | Shell command built from untrusted input |
| MCP103 | Unsafe deserialization (pickle.loads, unsafe yaml.load) |
| MCP104 | Aliased Python subprocess call with shell=True |
| MCP201 | Hardcoded secret or credential |
| MCP301 | Server bound to all network interfaces |
| MCP302 | Authentication / trust check disabled |
| MCP303 | MCP package launched with npx/uvx without an exact version pin |
| MCP304 | MCP launch command downloads a script and pipes it to a shell |
| MCP305 | MCP server uses plain HTTP to a non-local endpoint |
| MCP306 | MCP server runs with sudo or has root/home directory access |
Plus any [[custom_rules]] you define.
Design
Deliberately pattern/regex-based rather than a full taint-tracking analyser. That's a real tradeoff, stated plainly:
- You get: sub-second scans, a rule set you can read end-to-end in one sitting, no compilation or language runtime per target, trivial extensibility.
- You give up: certainty. It cannot tell you whether attacker-controlled
data actually reaches a
shell=Truecall. It is a first pass that tells you where a human should look — not a proof of safety, and it makes no soundness claim.
Some Python execution-call recognition uses the standard-library AST to
resolve imported subprocess aliases and multiline calls. MCP104 confirms
shell=True, but does not prove that the command is attacker-controlled.
MCP006 currently analyzes descriptions in JSON protocol manifests; it does not
evaluate prompt bodies or dynamically generated content. MCP007 flags explicit
additionalProperties: true in nested schemas; it does not infer whether an
application validates the value elsewhere. MCP104 recognizes imported Python
subprocess aliases only; alias resolution does not cover arbitrary dynamic
references.
Anyone selling you a scanner that claims to be complete is selling you something else.
Contributing
Issues and PRs welcome, especially new rules, more language coverage (Python / JS / TS today), and real-world servers that break it. See CONTRIBUTING.md for setup and the fixture-pair pattern every rule follows, or pick up a good first issue.
False positives are treated as real bugs — report them.
License
MIT — see LICENSE.
Metadata
Release files for mcp-triage 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| mcp_triage-0.5.0.tar.gz | 50.3 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mcp_triage-0.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 89.2 kB
Release files / mcp_triage-0.5.0.tar.gz
| Download URL | mcp_triage-0.5.0.tar.gz |
|---|---|
| Size | 50.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
96be73588da55d102a7dcd3afac35508a3ee9ea4eb5e0be26404a1118db9cbb8
|
|
BLAKE2b-256 checksum How to use checksums |
0d8c537aa124109968d91ad15cceb8d1f4ea2c6240c1a767d184a95608146f41
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.
Transparency logRelease files / mcp_triage-0.5.0-py3-none-any.whl
| Download URL | mcp_triage-0.5.0-py3-none-any.whl |
|---|---|
| Size | 39.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9649370be832eb5cc4d96ff2ba821956b511b49c7fb2d7a1de3d524d921f57e2
|
|
BLAKE2b-256 checksum How to use checksums |
fdb57d6d10086dbb7168d343d44bc0523b04e05461d743aa85cf238c99219ec3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.
Transparency logSecurity for mcp-triage 0.5.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for mcp_triage-0.5.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.