Metadata-Version: 2.4
Name: mcp-triage
Version: 0.4.0
Summary: Security scanner for MCP (Model Context Protocol) servers
Author: Yash Gautam
License: MIT License
        
        Copyright (c) 2026 YashkantG
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://github.com/YashkantG/mcp-triage
Project-URL: Repository, https://github.com/YashkantG/mcp-triage
Project-URL: Issues, https://github.com/YashkantG/mcp-triage/issues
Keywords: mcp,model-context-protocol,security,static-analysis,llm,ai-agents
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Topic :: Security
Classifier: Intended Audience :: Developers
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: typer>=0.12
Requires-Dist: rich>=13.0
Requires-Dist: tomli>=2.0; python_version < "3.11"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Dynamic: license-file

# mcp-triage

[![CI](https://github.com/YashkantG/mcp-triage/actions/workflows/ci.yml/badge.svg)](https://github.com/YashkantG/mcp-triage/actions/workflows/ci.yml)
[![MCP security](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/YashkantG/mcp-triage/main/.github/badges/self-scan.json)](#show-your-posture)
[![PyPI](https://img.shields.io/pypi/v/mcp-triage)](https://pypi.org/project/mcp-triage/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](pyproject.toml)

**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.

![mcp-triage demo](docs/demo.gif)

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](https://mcp.so) 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:

```bash
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](#runs-entirely-on-your-machine).

## 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.
- 🔓 **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)`, `os.system`, `pickle.loads`, unsafe `yaml.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_auth` flags
  left on.

Full [rule reference](#rules) below. Every rule ID is stable — see the
[rule stability policy](CHANGELOG.md#rule-stability-policy) before you pin one
in CI.

## Use it in CI

**GitHub Action** — no `pip install` boilerplate:

```yaml
- 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:

```yaml
- uses: YashkantG/mcp-triage@main
  with:
    path: ./my-mcp-server
    format: sarif
    upload-sarif: "true"
```

**pre-commit**:

```yaml
repos:
  - repo: https://github.com/YashkantG/mcp-triage
    rev: v0.3.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.

## Show your posture

If you publish an MCP server, `--format badge` emits a
[shields.io endpoint](https://shields.io/badges/endpoint-badge) payload you can
commit and display, so the people evaluating your server can see it was
checked:

```bash
mcp-triage scan . --format badge > .github/badges/mcp-security.json
```

```markdown
[![MCP security](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/OWNER/REPO/main/.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:

```python
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:

```toml
[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"
```

**CLI**, for one-offs: `mcp-triage scan . --ignore-rule MCP004`

This repo's own [`.mcptriage.toml`](.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](https://docs.pypi.org/trusted-publishers/) (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](SECURITY.md).

## What the MCP ecosystem actually looks like

`research/ecosystem_scan.py` surveys public MCP servers harvested from the
curated awesome-lists. Across a seeded sample of **141 successfully scanned
repositories** (from a pool of 3,701):

| | |
|---|---|
| Repositories with at least one finding | **41%** |
| Most widespread: dangerous code sinks (MCP101/102) | 34 and 30 repos |
| Hardcoded secrets (MCP201) | 13 repos |
| **Prompt injection (MCP001)** | **1 repo** |

Read those numbers with the caveats they deserve, because they cost something
to learn:

- **They are unverified automated output**, not audited vulnerabilities. The
  first pass of this survey reported 37% and 931 HIGH findings — then
  inspection showed most were fake credentials in test fixtures, `eval` in
  benchmark harnesses, and `pattern.exec(line)` (the JavaScript RegExp API,
  which alone accounted for 77% of the code-execution hits). Those became
  [precision fixes](tests/test_precision.py), not a blog post.
- **MCP001 firing once in 141 repos is the honest headline.** Deliberate tool
  poisoning is an adversarial attack, and public repositories are mostly
  written by people acting in good faith. The rule exists for the server you
  *didn't* expect to be hostile — not because the ecosystem is full of them.
- No repository is named here. If this tool finds something real in someone
  else's server, [disclose it to them privately](SECURITY.md).

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 |
| MCP101 | Dangerous code execution sink (`eval`, `exec`, `new Function`) |
| MCP102 | Shell command built from untrusted input |
| MCP103 | Unsafe deserialization (`pickle.loads`, unsafe `yaml.load`) |
| MCP201 | Hardcoded secret or credential |
| MCP301 | Server bound to all network interfaces |
| MCP302 | Authentication / trust check disabled |

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=True` call. It is a first pass that tells you
  where a human should look — not a proof of safety, and it makes no soundness
  claim.

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](CONTRIBUTING.md) for setup and the fixture-pair pattern every
rule follows, or pick up a
[good first issue](https://github.com/YashkantG/mcp-triage/labels/good%20first%20issue).

False positives are treated as real bugs — [report them](https://github.com/YashkantG/mcp-triage/issues/new/choose).

## License

MIT — see [LICENSE](LICENSE).
