Metadata-Version: 2.4
Name: mcp-tool-auditor
Version: 1.9.0
Summary: MCP Tool Poisoning Scanner — Defensive scanning + offensive pentest tooling for MCP servers. Maps to OWASP MCP Top 10.
Author: Përparim Mjeku
License: MIT
Project-URL: Homepage, https://github.com/perparimmjeku/mcp-auditor
Project-URL: Repository, https://github.com/perparimmjeku/mcp-auditor
Project-URL: Issues, https://github.com/perparimmjeku/mcp-auditor/issues
Keywords: mcp,security,tool-poisoning,owasp,penetration-testing,llm,ai-security
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Information Technology
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pyyaml>=6.0
Requires-Dist: requests>=2.31.0
Provides-Extra: llm
Requires-Dist: anthropic>=0.40; extra == "llm"
Provides-Extra: tokenizers
Requires-Dist: tokenizers>=0.15; extra == "tokenizers"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: black>=23.0; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Requires-Dist: mypy>=1.8; extra == "dev"
Requires-Dist: types-requests; extra == "dev"
Requires-Dist: types-PyYAML; extra == "dev"
Requires-Dist: build>=1.0; extra == "dev"
Requires-Dist: anthropic>=0.40; extra == "dev"
Requires-Dist: tokenizers>=0.15; extra == "dev"
Dynamic: license-file

# MCP Tool Auditor

**Scan MCP server tool definitions for poisoning, injection, and OWASP MCP Top 10 vulnerabilities.**

[![CI](https://github.com/perparimmjeku/mcp-auditor/actions/workflows/ci.yml/badge.svg)](https://github.com/perparimmjeku/mcp-auditor/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/mcp-tool-auditor)](https://pypi.org/project/mcp-tool-auditor/)
[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](https://python.org)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![OWASP MCP Top 10](https://img.shields.io/badge/OWASP-MCP03%20Tool%20Poisoning-red)](https://owasp.org/www-project-mcp-top-10/)

> **Most MCP scanners read tool definitions. This one also watches tool _behavior_** — it calls tools and catches Advanced Tool Poisoning Attacks (ATPA) that look benign on paper and turn malicious at runtime.

`mcp-tool-auditor` is a comprehensive security scanner for Model Context Protocol (MCP) servers. It detects tool poisoning, full-schema poisoning (FSP), rug-pull attacks (including tool shadowing and tampered baselines), cross-tool composition risk, and ATPA (Advanced Tool Poisoning Attacks) — across tool, resource, prompt, and server-instruction definitions, not just tool descriptions — while mapping findings to the OWASP MCP Top 10 framework.

The project also includes authorized offensive tooling for penetration testers and security researchers to simulate realistic MCP attack scenarios in controlled environments.

---

## Features

### Defensive Scanner

| Feature | What It Detects |
|---|---|
| Static Signatures | 40+ patterns such as "ignore security", "always use this tool", "send full conversation", and "authoritative source" |
| Heuristic Analysis | Imperative language scoring, authority spoofing, Unicode hidden characters, and excessive agency claims |
| Multi-Surface Scanning | The same signature/heuristic engine also runs on `resources`, `prompts`, and the server's `initialize` `instructions` string — not just tool descriptions |
| Schema Anomaly Detection | Full-Schema Poisoning (FSP), sidenote parameter injection, enum injection, poisoned defaults, and required-array abuse |
| Special Token Injection (STI) | Chat-template control tokens (`<\|im_start\|>`, `[INST]`, DeepSeek's fullwidth `<｜User｜>`, ...) via exact/Unicode-normalized/structural/encoded matching, plus an optional real-tokenizer-backed tier — in definitions *and* tool call output |
| Cross-Tool Composition Risk | Flags a server exposing both a sensitive-data-access tool and an egress-capable tool — individually benign, chainable into exfiltration |
| **Cross-Server Toxic-Flow Analysis** | Composition risk, generalized across the whole multi-server tool surface an agent session actually sees — a data-reading tool on one server and an egress tool on a different server, neither individually poisoned. On by default for `scan config`/`scan local`; CRITICAL/HIGH when the two tools' descriptions reference each other by name, MEDIUM for a generic pairing |
| **`inventory` — Host Discovery & Blast-Radius Mapping** | Every MCP server configured on the machine, its declared capabilities, its blast radius if compromised, and the worst cross-server chain reaching it — a host-risk report *and* a Mermaid graph, generated with zero execution by default. `--probe` (gated, opt-in) connects read-only to confirm the static guess against real tool data |
| Rug-Pull Detection | HMAC-signed SHA-256 schema fingerprinting; `check` detects both unexpected tool changes *and* tampered baseline files |
| Behavioral / ATPA Detection | Calls tools and inspects responses for the benign→malicious time-bomb signature, output injection, and exfil instructions |
| LLM Semantic Judge (opt-in) | `--llm-judge` sends descriptions to Claude to catch paraphrased poisoning that dodges static signatures |
| OAuth 2.1 Detection | Reports MCP 2025-06-18 OAuth-protected servers clearly (401 + `WWW-Authenticate`) instead of a generic error |
| Continuous Monitoring | `watch` re-scans on an interval and alerts a webhook on newly-observed findings |
| Engagement Scope Guardrails | `--engagement` refuses to scan targets outside an authorized-scope file |
| Client-Ready Reporting | `--format pentest` — engagement header, executive summary, methodology, evidence, remediation |
| Signed, Verifiable Reports | `--sign` HMAC-signs a canonical findings+scope+version payload (not the report bytes) into a `.sig` sidecar; `verify-report` checks it VALID/TAMPERED/INVALID. Report stays freely editable — only the findings/scope are attested |
| Retest Workflow | `retest --baseline <report>` diffs a re-scan into Fixed / Still Present / New |
| OWASP Mapping | Labels findings across the OWASP MCP Top 10 (active detection for MCP01, MCP02, MCP03, MCP05) |

### Offensive Tooling

| Tool | What It Simulates |
|---|---|
| Description Injection Server | Classic Tool Poisoning Attacks using malicious tool descriptions |
| FSP Generator | CyberArk-style schema poisoning variants |
| ATPA Server | Behavioral poisoning that becomes malicious after a configurable threshold |
| Rug-Pull Server | Serves clean tools initially, then swaps to malicious definitions |
| Tool Shadowing Server | Mimics trusted tool names while embedding injected payloads |
| STI Simulation Server | Benign tool definitions; injects a chat-template control token into responses after a configurable call threshold |

---

## Installation

### From PyPI (recommended)

```bash
pip install mcp-tool-auditor
```

Or run it without installing anything into your environment, via [uv](https://docs.astral.sh/uv/):

```bash
uvx mcp-tool-auditor --help
```

The default install is offline and needs no API key or token — it's `pyyaml` and
`requests`, nothing else. The optional LLM semantic judge (`--llm-judge`) is a separate
extra so it doesn't pull in an API client or require credentials for everyone:

```bash
pip install 'mcp-tool-auditor[llm]'
```

### From source (development)

```bash
git clone https://github.com/perparimmjeku/mcp-auditor.git
cd mcp-auditor
python -m pip install -e ".[dev]"
python -m pytest
```

See [CONTRIBUTING.md](CONTRIBUTING.md) for the full development workflow and
[RELEASING.md](RELEASING.md) for how versions get published to PyPI.

---

## Quick Start

### Scan an MCP Server

```bash
mcp-tool-auditor scan url http://localhost:8080/mcp
```

### Generate a Markdown Report

```bash
mcp-tool-auditor scan url http://localhost:8080/mcp \
  --format markdown \
  -o report.md
```

### Scan MCP Servers from a Configuration File

```bash
mcp-tool-auditor scan config \
  ~/.config/Claude/claude_desktop_config.json
```

Every server in the config is scanned, then the COMBINED tool surface is checked for
cross-server toxic flows — see below.

### Cross-Server Toxic-Flow Analysis

A real agent session has tools from several MCP servers active at once. A tool that
reads secrets/files/env/browser data on one server and a tool that can send data out
(HTTP, email, webhook) on a *different* server form an exfiltration path that no
single-server scan can see, even though neither tool looks poisoned on its own —
that's what `scan config`/`scan local` check automatically:

```bash
mcp-tool-auditor scan config ~/.config/Claude/claude_desktop_config.json
# FLOW_SENSITIVE_SINK   (MEDIUM)   generic source+sink pairing across two servers
# FLOW_CROSS_SERVER_EXFIL (HIGH/CRITICAL) one tool's description names the other —
#                                  evidence the pairing is wired together, not incidental
```

Every tool across the whole scan is tagged SOURCE (reads sensitive data), SINK (can
egress data), or SENSITIVE_ACTION (destructive/state-changing) from its
name/description/schema — the same signature-matching approach as the rest of the
scanner, not a fresh classifier. On by default (pure local static analysis over tool
definitions already fetched during the scan — no extra cost, no external call); disable
with `--no-cross-server-flow`. A same-server pairing is `COMPOSITION_CONFUSED_DEPUTY`'s
job, not this — a pair only counts here if the two tools come from different servers.
Findings are suppressible like any other rule (`--suppress FLOW_SENSITIVE_SINK`).

### Host Inventory & Blast-Radius Mapping

`inventory` turns config discovery into a recon deliverable: every MCP server configured
on the machine, what it can read/reach if compromised, and the worst cross-server chain
threatening it — with **zero execution** by default:

```bash
mcp-tool-auditor inventory --format markdown
```

Every server gets tagged SOURCE/SINK/SENSITIVE_ACTION from its launch command/args/env-var
**names alone** (never values) — a guess, not a fact, since no tool has actually run. That
guess is never presented as confirmed: it's badged 🔍 INFERRED everywhere (report prose,
JSON field, graph node/edge style), capped at `INV_INFERRED_CHAIN` (MEDIUM, hard ceiling —
inference alone can never reach HIGH/CRITICAL), and every message says to run `--probe` to
confirm it. Here's what that looks like for two servers whose launch config cross-references
each other:

```mermaid
flowchart LR
    classDef confirmedNode fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px;
    classDef inferredNode fill:#f5f5f5,stroke:#9e9e9e,stroke-width:1px,stroke-dasharray:4 2;

    fs_server["fs-server<br/>SOURCE"]
    class fs_server inferredNode
    http_server["http-server<br/>SINK"]
    class http_server inferredNode

    fs_server -->|"MEDIUM (possible)"| http_server
    linkStyle 0 stroke:#9e9e9e,stroke-width:2px,stroke-dasharray:5 3;
```

`--probe` (opt-in, gated behind the same authorization ack as `behavior`/`attack`) connects
read-only — enumeration only, never calls a tool — and CONFIRMS the guess against real
`tools/list` data. A confirmed, name-coupled chain renders completely differently: solid
border, ✅ CONFIRMED, red, `FLOW_CROSS_SERVER_EXFIL` at CRITICAL:

```mermaid
flowchart LR
    classDef confirmedNode fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px;
    classDef inferredNode fill:#f5f5f5,stroke:#9e9e9e,stroke-width:1px,stroke-dasharray:4 2;

    fs_server["fs-server<br/>SOURCE"]
    class fs_server confirmedNode
    http_server["http-server<br/>SINK"]
    class http_server confirmedNode

    fs_server -->|"CRITICAL"| http_server
    linkStyle 0 stroke:#c62828,stroke-width:2px,stroke-dasharray:0;
```

```bash
mcp-tool-auditor inventory --probe --yes --format markdown
```

A benign host (no SOURCE+SINK pairing anywhere) reads as low-risk, not just quiet on
one rule — no chain, no colored edge, an isolated grey node. `--format json`/`sarif` give
the same data machine-readably (deterministic, secret-redacted by construction) for
SOC/CMDB/DefectDojo ingestion; `--suppress`/`--suppressions` work the same as everywhere
else.

### Register a Baseline Fingerprint

```bash
mcp-tool-auditor register url http://localhost:8080/mcp
```

Baselines are HMAC-SHA256 signed, so `check` can tell a legitimate tool change from a
tampered/forged baseline file (`RUGPULL_BASELINE_TAMPERED`). By default the signing key is
a local file (`~/.mcp-tool-auditor/fingerprints/.hmac_key`); for CI use, supply your own
out-of-band key so the key and the baseline file don't share a trust boundary:

```bash
export MCP_TOOL_AUDITOR_BASELINE_KEY="$(openssl rand -hex 32)"   # store in your CI secrets
```

### Check for Rug-Pull Changes

```bash
mcp-tool-auditor check url http://localhost:8080/mcp
```

### Continuous Monitoring

Point-in-time scans miss drift that happens between runs. `watch` re-scans on an interval
and POSTs newly-observed findings to a webhook (Slack/Discord/PagerDuty/anything with a
webhook endpoint):

```bash
mcp-tool-auditor watch url http://localhost:8080/mcp \
  --interval 300 \
  --check-rugpull \
  --webhook https://hooks.example.com/services/T000/B000/XXXX
```

Each finding is alerted once (in-memory dedup for the process's lifetime), so a standing
issue doesn't repeat-alert every interval.

### Behavioral / ATPA Detection

Static scans read tool *definitions*; behavioral probing actually calls tools and
inspects their *responses* to catch Advanced Tool Poisoning Attacks (ATPA) — tools
that act benign, then return poisoned output after a few calls.

```bash
# Live probe (executes real tool calls — authorized testing only)
mcp-tool-auditor behavior url http://localhost:8080/mcp --calls 6 --yes
mcp-tool-auditor behavior stdio --calls 6 -- npx @modelcontextprotocol/server-everything

# Offline: analyze a recorded transcript (tool name -> list of response strings)
mcp-tool-auditor behavior import responses.json --format json
```

Findings: `BEHAV_ATPA_TRANSITION` (benign→malicious time-bomb), `BEHAV_OUTPUT_INJECTION`,
`BEHAV_RESPONSE_DIVERGENCE`, and `BEHAV_CALL_ERROR`. Live modes require an
authorization acknowledgement (`--yes` or `MCP_TOOL_AUDITOR_ASSUME_AUTHORIZED=1`).

### Authenticated Scanning (real engagements)

```bash
# Auth header + route through Burp; tune signal with confidence and suppressions
mcp-tool-auditor scan url https://target/mcp \
  -H "Authorization: Bearer $TOKEN" \
  --proxy http://127.0.0.1:8080 \
  --min-confidence HIGH \
  --suppress HEUR_IMPERATIVE
```

Findings carry a **confidence** level (HIGH/MEDIUM/LOW); `--min-confidence` filters
noisy heuristics and `--suppressions <file>` silences accepted false positives.
URL transport speaks full MCP Streamable HTTP (session id + protocol-version headers) at
protocol version `2025-06-18`. If the server requires OAuth 2.1, the scan reports a clear
`OAUTH_REQUIRED` finding (with protected-resource metadata, if discoverable) instead of a
generic HTTP error — complete the OAuth flow yourself and pass the resulting bearer token
via `--header`; this tool does not perform interactive OAuth login.

### LLM Semantic Judge (opt-in)

Static signatures and heuristics match fixed phrases; an attacker can dodge them by
rephrasing the same intent differently. `--llm-judge` sends tool/resource/prompt text to
Claude and asks it to judge intent instead of pattern-match text:

```bash
pip install 'mcp-tool-auditor[llm]'
export ANTHROPIC_API_KEY="sk-ant-..."
mcp-tool-auditor scan url http://localhost:8080/mcp --llm-judge
```

This is opt-in only and never runs by default — it sends third-party server content to
Anthropic's API, which is a data-handling decision the operator must make explicitly.
Flagged items get the `LLM_SEMANTIC_POISONING` rule (MEDIUM confidence; review the exact
wording before rejecting a tool on this alone).

### Real Pentest Engagements: Scope, Client Reports, Retest

Three pieces built specifically for running this as part of a real, authorized
engagement rather than a CI job:

**1. Engagement/scope file** — declare the authorized scope once; scans against a
stdio/url target outside it are refused before anything touches the network:

```json
{
  "client": "Acme Corp",
  "tester": "J. Doe",
  "start_date": "2026-07-01",
  "end_date": "2026-07-15",
  "notes": "Authorized MCP tool poisoning assessment, ref. SOW-1234",
  "allowed_targets": ["https://*.acme-client.example.com/*", "https://mcp.acme.example.com/mcp"]
}
```

```bash
mcp-tool-auditor --engagement engagement.json scan url https://mcp.acme.example.com/mcp
# scan url https://other-company.example.com/mcp would be refused before connecting
```

`allowed_targets` entries are exact strings or shell-glob patterns. An engagement file
with no `allowed_targets` still works — it just carries report metadata (client/tester/
dates) without restricting scope. Scope is enforced for `scan stdio`/`url`, `behavior
stdio`/`url`, `watch url`, `register`, and `check`; `scan config`/`local`/`watch config`
don't currently enforce it per-server since they aggregate arbitrary local configs.

**2. Client-ready reports** — `--format pentest` produces an engagement header,
executive summary, methodology section, and full evidence (the actual tool/resource/
prompt text) plus remediation guidance per finding — built to hand to a client, not
just a CI log:

```bash
mcp-tool-auditor --engagement engagement.json scan url https://mcp.acme.example.com/mcp \
  --format pentest -o report.md
```

**3. Retest** — after the client remediates, re-scan and diff against the original
report to show what's Fixed / Still Present / New:

```bash
mcp-tool-auditor scan url https://mcp.acme.example.com/mcp --format json -o baseline.json
# ... client remediates ...
mcp-tool-auditor retest --baseline baseline.json url https://mcp.acme.example.com/mcp \
  --format pentest -o retest-report.md
```

`--fail-on` on `retest` gates on unresolved findings only (Still Present + New) — a
clean retest where everything's Fixed exits 0.

**4. Signed, verifiable reports** — `--sign` writes a chain-of-custody sidecar
alongside any report (`scan`/`retest`/`source-scan`, any `--format`), binding the
findings, engagement scope, and tool version:

```bash
mcp-tool-auditor --engagement engagement.json scan url https://mcp.acme.example.com/mcp \
  --format pentest -o report.md --sign
# writes report.md AND report.md.sig

mcp-tool-auditor verify-report report.md.sig
# Status: VALID
```

**What's attested** — not the report's bytes. `report.md.sig` signs a canonical,
deterministic payload (every finding + which server it came from + the engagement's
client/tester/dates/scope + the tool version), built straight from the same data the
report renders from. The human report stays freely editable — reformat it, add a
letterhead, export to PDF, annotate it for legal review — none of that touches the
signature, because the signature was never over the report's text in the first place.
What *does* invalidate it: changing a finding's severity/rule/message, re-attributing a
finding to a different server (scope tampering), or editing the engagement metadata
after the fact.

`verify-report` reports `VALID`, `TAMPERED` (the payload was altered after signing —
right key, wrong data), or `INVALID` (wrong key, or a malformed sidecar) — exit code 0
for VALID, 2 for anything else, so it drops straight into a CI gate.

**Key handling** — same trust-boundary model as the rug-pull baseline key, but a
*separate* key (`MCP_TOOL_AUDITOR_REPORT_KEY`, vs. baselines' `..._BASELINE_KEY`): a
report signature may need to leave your machine entirely so a client can verify it
independently, and handing out the same key that also protects local rug-pull baseline
integrity would put an unrelated trust boundary at risk. Auto-generated and persisted
locally (`~/.mcp-tool-auditor/reports/.hmac_key`, `0600` permissions) if you don't
supply one; for anything beyond solo local use, set `MCP_TOOL_AUDITOR_REPORT_KEY`
out-of-band (a CI secret, a password manager) so the key and the reports it signs don't
share a trust boundary — same reasoning as the baseline key. The key is never printed
or written into the sidecar; only a `key_id` (a short, one-way fingerprint) is, so a
verifier can tell *which* key produced a signature without ever seeing it.

### Auto-Discover and Scan Local MCP Configs

```bash
# Finds and scans configs for Claude Desktop, Cursor, VS Code, Windsurf, Zed
mcp-tool-auditor scan local
```

### Export SARIF and Explain Findings

```bash
# SARIF for GitHub code-scanning / GitLab / DefectDojo ingestion
mcp-tool-auditor scan import tools.json --format sarif -o results.sarif

# Get remediation guidance for any finding rule
mcp-tool-auditor explain BEHAV_ATPA_TRANSITION
mcp-tool-auditor explain --list
```

SARIF 2.1.0 output is deterministic — `driver.rules[]` sorted by rule id and
`results[]` sorted by `(ruleId, tool_name, field, message)`, so re-scanning
unchanged findings produces byte-identical output and diffs cleanly in CI. Each
`reportingDescriptor` carries `helpUri` (links `docs/RULES.md`) alongside the existing
remediation text, plus an empty `atlas_ids` placeholder in `properties` so a future
MITRE ATLAS mapping can be added without a schema change. Each result's `properties`
bag carries `owasp_id`, `attack_type`, `confidence`, and `retest_status` (present —
`null` on a plain scan, set on `retest --format sarif`) so nothing from a `Finding` is
lost in translation, and DefectDojo's generic SARIF importer picks all of it up.

### Source-Scan (Prompt-In-Shell-Out)

Goes one level deeper than definitions/behavior: reads an MCP server's **source code**
(Python via AST, JS/TS via heuristics) and flags shell-injection sinks fed by
LLM-controlled tool arguments — `subprocess(shell=True)`, `os.system`, `child_process.exec`, etc.

```bash
mcp-tool-auditor source-scan ./my-mcp-server                 # human-readable
mcp-tool-auditor source-scan ./my-mcp-server --format sarif  # code-scanning
mcp-tool-auditor source-scan ./my-mcp-server --fail-on CRITICAL  # CI gate
```

It only opens files that import an MCP SDK, so it won't flag shell calls in unrelated code.

### Special Token Injection (STI)

Chat-template control tokens (`<|im_start|>`, `[INST]`, `<|start_header_id|>`, DeepSeek's
fullwidth `<｜User｜>`, and more — registry in `signatures/sti_tokens.yaml`, grouped by
model family, easy to extend via PR) spoof or close a conversation turn in whatever prompt
an MCP client eventually builds from tool/resource/prompt text. Runs by default across all
four surfaces, plus tool call *output* (`behavior` command) to catch a token a tool only
emits after N calls:

```bash
mcp-tool-auditor scan url http://localhost:8080/mcp          # exact/normalized/structural tiers
mcp-tool-auditor scan url http://localhost:8080/mcp --sti-decode  # + bounded base64/hex tier
mcp-tool-auditor behavior url http://localhost:8080/mcp --calls 6 --yes  # scans tool output too
```

`--sti-decode` is opt-in and off by default: it only considers length-banded base64/hex
substrings (never "decode every string"), and only compares decoded bytes against the
token registry, never the looser structural shape check — bounding both cost and false
positives. Findings: `STI_EXACT`/`STI_NORMALIZED` (HIGH confidence — obfuscation is *more*
suspicious than the plain token, not less), `STI_STRUCTURAL`/`STI_ENCODED` (MEDIUM). A tool
that legitimately documents a token (e.g. a Llama-2 prompt-formatting helper mentioning
`[INST]`) still produces a finding — detection can't tell intent from text alone — but it's
suppressible like any other rule (`--suppress STI_EXACT` or a suppressions file entry).

### Tokenizer-Aware STI (optional)

The tiers above answer "does this text *look like* a known control token?" by string
matching. `--sti-tokenizer` answers a stronger question: **will this string actually be
parsed as a special token by the tokenizer a target deployment runs**, rather than being
split into ordinary byte-pair pieces? That's deployment-relative — `<|im_start|>` is one
reserved token under ChatML/Qwen and inert plaintext under a tokenizer that's never heard
of it — and it requires a *real* tokenizer's real vocabulary to answer; no amount of
string-matching against our own token registry can (seeding a tokenizer with our own
strings and checking they round-trip would only ever confirm what the string tiers
already catch).

```bash
pip install 'mcp-tool-auditor[tokenizers]'
mcp-tool-auditor scan url http://localhost:8080/mcp --sti-tokenizer chatml,mistral
```

`--sti-tokenizer` takes a comma list of target families: `chatml`, `qwen` (alias for the
same ChatML-family tokenizer), `mistral`, `deepseek`. Each resolves to a real, offline,
license-verified `tokenizer.json` vendored in the package (see
`tokenizer_assets/THIRD_PARTY_NOTICES.md` for source/license per file) — loaded via
`Tokenizer.from_str()` and `importlib.resources`, **never** `Tokenizer.from_pretrained()`,
**never** a network call, at scan time or anywhere else. `llama3` and `gemma` are
recognized names with no offline-redistributable asset yet (Meta's Llama Community
License and Gemma's Terms of Use both require attribution/notice machinery incompatible
with a silent `pip install`) — requesting them prints a clear message and the four string
tiers still cover that family's known tokens. Without the `[tokenizers]` extra installed,
`--sti-tokenizer` does the same: warns with an install hint, never crashes, never disables
anything else.

What confirmation actually changes: a string-tier match that a real target tokenizer
resolves to an actual special/added-vocabulary token id is upgraded to one `STI_TOKENIZER`
finding (HIGH confidence — a confirmed resolution against the real target is the strongest
signal this scanner has). A string-tier match the tokenizer does **not** confirm is left
completely unchanged — that divergence is itself signal, not something to hide. Concretely:
`[INST]`/`[/INST]` are Llama-2-style prompt-template convention, not tokens Mistral's real
tokenizer treats specially — `--sti-tokenizer mistral` correctly leaves them as plain
`STI_EXACT`, while `--sti-tokenizer chatml` in the same scan upgrades real ChatML control
tokens to `STI_TOKENIZER`. The tier can also catch tokens the string registry has never
heard of: it encodes the *entire* text and checks every resulting token id against the
target's real added-vocabulary set, not just our own regex/registry candidates.

### Generate Offensive Test Servers

```bash
mcp-tool-auditor generate all --output-dir ./pentest_servers
```

---

## CI/CD Integration

Gate merges on poisoning findings and upload results to GitHub Security.

### GitHub Action

```yaml
# .github/workflows/mcp-audit.yml
name: MCP Audit
on: [push, pull_request]

jobs:
  audit:
    runs-on: ubuntu-latest
    permissions:
      security-events: write   # to upload SARIF
    steps:
      - uses: actions/checkout@v4

      - name: Scan tool definitions
        uses: perparimmjeku/mcp-auditor@main
        with:
          command: scan import tools.json --format sarif -o mcp.sarif --fail-on HIGH

      - name: Upload SARIF
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: mcp.sarif
```

> Until the package is on PyPI, point the action at the repo:
> `with: { install-spec: "git+https://github.com/perparimmjeku/mcp-auditor.git" }`

### Plain CLI in any pipeline

```bash
# Exit code 2 if any HIGH+ finding is present — fails the build
mcp-tool-auditor scan import tools.json --fail-on HIGH
```

### Verbose Output, Logs, and Metrics

```bash
mcp-tool-auditor -v scan import tests/fixtures/poisoned_tools.json
```

By default, structured logs are written under `~/.mcp-tool-auditor/logs` and scan
metrics are appended to `~/.mcp-tool-auditor/metrics.jsonl`. Use `--no-log-file`
or `--no-metrics` to disable those side effects for a run.

### Configuration

```bash
mcp-tool-auditor --config ./config.yaml scan url http://localhost:8080/mcp
```

The CLI accepts JSON or YAML config files. CLI flags still take precedence for
scan output format and minimum severity.

### Offensive Simulation Acknowledgement

```bash
mcp-tool-auditor attack --yes atpa --port 8080 --threshold 3
```

Offensive simulation servers show an authorization warning before they start.
Use `--yes` only for non-interactive, authorized lab runs.

---

## Detection Coverage & Sample Reports

- **[docs/RULES.md](docs/RULES.md)** — full catalog of all 65 detection rules with confidence levels.
- **Sample output** from a poisoned fixture: [markdown](docs/samples/sample-report.md) ·
  [json](docs/samples/sample-report.json) · [sarif](docs/samples/sample-report.sarif).

---

## OWASP MCP Top 10 Mapping

| OWASP ID | Issue | Detection |
|---|---|---|
| MCP01 | Token Mismanagement & Secret Exposure | `ST_IGNORE_PREVIOUS`, `ST_BYPASS`, `ST_IGNORE_SECURITY` |
| MCP02 | Privilege Escalation via Scope Creep | `HEUR_AGENCY`, `COMPOSITION_CONFUSED_DEPUTY`, `FLOW_CROSS_SERVER_EXFIL`/`FLOW_SENSITIVE_SINK`/`INV_INFERRED_CHAIN`, credential-related signatures |
| MCP03 | Tool Poisoning | Signatures, heuristics, FSP, rug-pull/shadowing, STI (`STI_EXACT`/`STI_NORMALIZED`/`STI_STRUCTURAL`/`STI_ENCODED`), and behavioral/ATPA detection |
| MCP05 | Command Injection & Execution | `ST_EXECUTE`, `ST_CODE_EXEC`, command-related schema findings |

Findings are actively emitted for **MCP01, MCP02, MCP03, and MCP05**. Reports also
recognize the remaining OWASP MCP Top 10 IDs (MCP04, MCP06–MCP10) by label, but the
current analyzers do not yet emit findings for them.

---

## Offensive Testing Examples

```bash
# Start an ATPA simulation server
mcp-tool-auditor attack atpa --port 8080 --threshold 3

# Start a rug-pull simulation server
mcp-tool-auditor attack rugpull --port 8081 --switch-after 5

# Start an STI (Special Token Injection) simulation server -- benign
# definitions, injects a chat-template control token into responses
# after --threshold calls
mcp-tool-auditor attack sti --port 8082 --threshold 3

# Generate standalone attack servers
mcp-tool-auditor generate all --output-dir ./test_servers

python ./test_servers/server_description_injection.py
```

---

## Architecture

```text
mcp-auditor/
├── mcp_tool_auditor/
│   ├── __init__.py
│   ├── cli.py
│   ├── config.py
│   ├── logging_config.py
│   ├── metrics.py
│   ├── security.py
│   ├── validation.py
│   ├── watch.py
│   ├── engagement.py
│   ├── auditor/
│   │   ├── __init__.py
│   │   ├── scanner.py
│   │   ├── models.py
│   │   ├── discovery.py
│   │   ├── inventory.py
│   │   ├── signing.py
│   │   ├── report_signing.py
│   │   ├── remediation.py
│   │   ├── analyzers/
│   │   │   ├── __init__.py
│   │   │   ├── static.py
│   │   │   ├── heuristic.py
│   │   │   ├── schema.py
│   │   │   ├── rugpull.py
│   │   │   ├── behavioral.py
│   │   │   ├── composition.py
│   │   │   ├── capability.py
│   │   │   ├── flow.py
│   │   │   ├── llm_judge.py
│   │   │   ├── sti.py
│   │   │   ├── sti_tokenizer.py
│   │   │   ├── surface.py
│   │   │   └── patterns.py
│   │   ├── signatures/
│   │   │   ├── __init__.py
│   │   │   ├── descriptions.yaml
│   │   │   └── sti_tokens.yaml
│   │   ├── tokenizer_assets/
│   │   │   ├── __init__.py
│   │   │   ├── THIRD_PARTY_NOTICES.md
│   │   │   ├── chatml_qwen.tokenizer.json
│   │   │   ├── mistral.tokenizer.json
│   │   │   └── deepseek.tokenizer.json
│   │   └── reporters/
│   │       ├── __init__.py
│   │       ├── json_reporter.py
│   │       ├── markdown_reporter.py
│   │       ├── pentest_reporter.py
│   │       ├── inventory_reporter.py
│   │       ├── inventory_graph.py
│   │       └── sarif_reporter.py
│   └── offensive/
│       ├── __init__.py
│       ├── poisoner.py
│       ├── atpa_server.py
│       ├── rugpull_sim.py
│       └── sti_server.py
├── tests/
│   ├── __init__.py
│   ├── conftest.py
│   ├── test_scanner.py
│   ├── test_patterns.py
│   ├── test_behavioral.py
│   ├── test_behavior_integration.py
│   ├── test_cli_behavior.py
│   ├── test_confidence_suppressions.py
│   ├── test_discovery.py
│   ├── test_engagement.py
│   ├── test_http_auth_session.py
│   ├── test_llm_judge.py
│   ├── test_pentest_reporter.py
│   ├── test_remediation.py
│   ├── test_retest.py
│   ├── test_rugpull_signing.py
│   ├── test_sarif_reporter.py
│   ├── test_scope_enforcement.py
│   ├── test_source_scan.py
│   ├── test_sti_analyzer.py
│   ├── test_sti_behavioral.py
│   ├── test_sti_offensive.py
│   ├── test_sti_tokenizer.py
│   ├── test_surface_scanning.py
│   ├── test_watch.py
│   └── fixtures/
│       ├── __init__.py
│       ├── poisoned_tools.json
│       └── source/
├── config.yaml
├── pyproject.toml
├── CONTRIBUTING.md
├── SECURITY.md
├── LICENSE
└── README.md
```

---

## Configuration

See `config.yaml` for example scanner settings. Pass a config file with `--config`
(JSON or YAML); CLI flags override the file for output format and minimum severity.

### Default Configuration

```yaml
auditor:
  severity_threshold: MEDIUM

  heuristic_analysis:
    description_length_threshold: 500
    imperative_threshold: 2

  schema_analysis:
    check_fsp_params: true
    check_required_array: true
    check_enum_values: true
```

---

## Contributing

Pull requests are welcome.

Please see `CONTRIBUTING.md` for development guidelines, coding standards, and testing procedures.

Security issues should follow `SECURITY.md`.

---

## License

MIT License — see the `LICENSE` file for details.

---

## Legal Disclaimer

This tool is intended strictly for:

- Authorized penetration testing
- Defensive security research
- Educational and lab environments
- Controlled validation of MCP security controls

Users must have explicit permission before testing any target systems.

The authors assume no liability for misuse, unauthorized testing, or damages caused by improper use of this software.

---

## Package Metadata

### `mcp_tool_auditor/__init__.py`

```python
"""MCP Tool Auditor package."""

__version__ = "1.8.0"
__author__ = "Përparim Mjeku"
__license__ = "MIT"
```
