# mcpprobe docs

> Version 0.1.0. HTML version: https://mcpprobe.tech/docs · Checks: https://mcpprobe.tech/checks · Scanning policy: https://mcpprobe.tech/scanning-policy

29 checks in the catalogue: 16 passive checks implemented in v0.1.0, 8 active checks coming soon, 5 attestation-only governance items.

## Quickstart

Only scan servers you own or are explicitly authorised to test. The default run is passive: it sends what a normal MCP client sends plus a few benign malformed requests, and never calls a tool.

```bash
# run a passive scan, no install needed
pipx run --spec mcpprobe-scanner mcpprobe https://your-server.example/mcp

# or install it and keep the reports
pipx install mcpprobe-scanner   # the command is mcpprobe
mcpprobe https://your-server.example/mcp --sarif mcpprobe.sarif --json mcpprobe.json

# a local stdio server you run yourself
mcpprobe --stdio "npx -y @your-scope/your-mcp-server@1.2.3"

# checks behind your own auth: pass a token you own
mcpprobe https://your-server.example/mcp --header "Authorization: Bearer $MCP_TEST_TOKEN"
```

Real output (excerpt) against the scanner's deliberately vulnerable test server:

```text
$ mcpprobe http://127.0.0.1:8765/mcp
mcpprobe 0.1.0 | passive mode | rate 5.0/s | scan only what you own or are authorised to test
transport=http | revision=2025-11-25 (handshake) | auth=none | server=fake-mcp 0.0.1 | tools=7
Findings  critical 0   high 8   medium 5   low 3   info 5
 HIGH      AUTH-01  Remote MCP endpoint accepts requests without authentication
 HIGH      META-01  Possible tool-description poisoning
 HIGH      META-05  Secret material in server metadata or configuration
 HIGH      TRANS-01 Origin header not validated (DNS-rebinding exposure)
 HIGH      TRANS-03 Forged session ID accepted
 MEDIUM    ERR-01   Verbose error responses leak stack traces, paths or versions
 MEDIUM    META-03  Tool annotations contradict the tool's apparent capability
 …
INJ-01     active  skipped  requires --active and verified ownership
SUPPLY-01  active  skipped  not applicable to http transport
38 request/response pairs recorded as evidence.
Result: FAIL (findings at or above high)
```

## Passive vs active

|  | Passive (default) | Active (--active) |
| --- | --- | --- |
| What it sends | What a normal client sends (negotiation, the */list methods, well-known metadata) plus a handful of benign malformed requests | Tool calls with non-destructive canaries (the checks are stubs in v0.1.0 and report not_implemented) |
| Calls tools | Never | Only when the active checks ship, and never tools passed to --exclude-tool |
| Requires | A server you own or are authorised to test | --authorised-by, --scope and --verify (dns-txt or local) |

In v0.1.0 the 8 active checks (INJ-01..05, SUPPLY-01/02, SANDBOX-01) are registered stubs: they appear in every report as skipped or not implemented, and no payloads are sent. The authorisation gate is already real and enforced in code.

## Verify ownership

Active mode is refused without verified ownership.

| Flag | For | What to publish |
| --- | --- | --- |
| `--verify dns-txt` | HTTP targets | _mcpprobe.your-server.example.  TXT  "mcpprobe-verification=<token>" |
| `--verify local` | stdio servers you run yourself | Nothing to publish: the server is a process on your own machine |

```bash
mcpprobe token your-server.example
# Publish this DNS TXT record, then run with --active --verify dns-txt:
#   _mcpprobe.your-server.example.  TXT  "mcpprobe-verification=<token>"

mcpprobe https://your-server.example/mcp --active --verify dns-txt \
  --authorised-by "platform security, CHG-1234" \
  --scope "https://your-server.example/mcp" --json scan.json
```

Every active run writes an authorisation record (`scan.authorisation.json` next to the report): who authorised it, the scope, the verification proof, rate cap and excluded tools.

## Commands and flags

```text
mcpprobe <url>                   passive scan of a remote MCP server
mcpprobe --stdio "<command>"     spawn and scan a local stdio server
mcpprobe checks                  list every check with mode, severity and standards
mcpprobe token <host>            create an ownership token for --verify dns-txt

  --header "Name: value"         extra HTTP header, e.g. a bearer token you own (repeatable)
  --env KEY=VALUE                environment for a stdio server (repeatable)
  --fail-on info|low|medium|high|critical   exit 1 at or above this severity (default high)
  --json FILE                    full JSON report, with evidence
  --sarif FILE                   SARIF 2.1.0 report
  --sarif-location PATH          repository file to anchor SARIF results to
  --baseline FILE                META-02 baseline to compare against and update
  --no-write-baseline            compare against --baseline without updating it
  --rate N                       requests per second (default 5, ceiling 10)
  --timeout SECONDS              per-request timeout (default 15)
  --only ID / --skip ID          run or skip specific checks (repeatable)
  --no-follow-as                 AUTH-03: do not fetch authorization-server metadata on third-party hosts
  --insecure                     do not verify the target's TLS certificate
  -v / -q                        verbose / quiet terminal output

  --active                       enable active checks; requires all three of:
  --authorised-by TEXT           who authorised this scan (name, role, ticket)
  --scope TEXT                   the agreed scope statement
  --verify dns-txt|local         ownership verification method
  --exclude-tool NAME            a tool that must never be called (repeatable)
```

## Exit codes

| Code | Meaning |
| --- | --- |
| 0 | No finding at or above --fail-on (default high) |
| 1 | At least one finding at or above --fail-on |
| 2 | Error: target unreachable or not MCP, bad arguments, ownership verification failed, authorisation refused, or the target opted out |

## Rug-pull baselines

```bash
mcpprobe https://your-server.example/mcp --baseline .mcpprobe/baseline.json
```

The first run stores canonical SHA-256 hashes of every tool, prompt, resource and the server instructions. Later runs report anything added, removed or changed and update the file; use `--no-write-baseline` to compare without updating.

## Reports and evidence

- `--json FILE`: the full report, including every request/response pair (timestamp, method, status, a header subset, bodies truncated to 4 KB). `Authorization` values and recognised secret patterns are redacted.
- `--sarif FILE`: SARIF 2.1.0.
- `-v`: descriptions and scan notes in the terminal; `-q`: no terminal output.

## GitHub Action

A Docker container action, so no Python setup is needed.

```yaml
name: MCP security
on: [push, pull_request]
permissions:
  contents: read
  security-events: write
jobs:
  mcpprobe:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: mcpprobe-dev/mcpprobe@v0.1.0
        with:
          target: https://staging.your-server.example/mcp
          fail-on: high
          sarif-output: mcpprobe.sarif
      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: mcpprobe.sarif
          category: mcpprobe
```

| Input | Meaning |
| --- | --- |
| `target` | MCP endpoint URL to scan (required) |
| `fail-on` | info, low, medium, high or critical (default high) |
| `rate` | requests per second, 1 to 10 (default 5) |
| `sarif-output` | SARIF file to write (default mcpprobe.sarif) |
| `sarif-location` | repository file to anchor results to |
| `json-output` | optional full JSON report |
| `baseline` | optional META-02 baseline file |
| `headers` | extra headers, one 'Name: value' per line, e.g. a test token from secrets |

## SARIF and code scanning

Results carry GitHub's `security-severity` on each rule and the OWASP IDs as tags. Because a scanned endpoint is not a file, results point at the target URL unless you pass `--sarif-location`.

| mcpprobe severity | SARIF level |
| --- | --- |
| Critical, High | `error` |
| Medium | `warning` |
| Low, Info | `note` |

## Badge

The badge reflects your latest hosted scan and turns grey 30 days after the last clean one. It links to the public report page, and a JSON endpoint lets directories read the status.

```markdown
[![mcpprobe](https://mcpprobe.tech/badge/your-server.example.svg)](https://mcpprobe.tech/r/your-server.example)
```

## API and MCP endpoint

```text
GET https://mcpprobe.tech/api/v1/status/your-server.example

{
  "host": "your-server.example",
  "status": "passing",
  "checked_at": "2026-10-19T07:40:00Z",
  "expires_at": "2026-11-18T07:40:00Z",
  "findings": { "critical": 0, "high": 0, "medium": 0, "low": 1 },
  "report_url": "https://mcpprobe.tech/r/your-server.example",
  "badge_url": "https://mcpprobe.tech/badge/your-server.example.svg",
  "scanner_version": "0.1.0"
}
```

- `GET /badge/{host}.svg` returns the badge: passing, failing, stale or unknown.
- `GET /scanning-ips.json` lists the addresses hosted scans come from.
- An MCP endpoint that lets agents read reports is planned; it is not live in 0.1.0.

Python API with the optional LLM-judge hook for META-01:

```python
import asyncio
from mcpprobe.checks import JudgeVerdict, ScanOptions
from mcpprobe.scanner import ScanConfig, run_scan


def judge(text: str, task: str) -> JudgeVerdict | None:
    return None  # plug in your own model here; mcpprobe makes no LLM calls


report, evidence = asyncio.run(
    run_scan(ScanConfig(target="https://your-server.example/mcp", options=ScanOptions(judge=judge)))
)
print(report.counts())
```

## Methodology and false-positive rates

Every check has a documented probe, a default severity, the evidence it records, known false positives and a fix; all of it is in the checks catalogue, generated from the scanner itself. Heuristic checks never rate a finding Critical on their own, and a check may raise or lower the severity of an individual finding (loopback-only servers are reported one level lower). We will measure each check's false-positive rate on a public dataset and publish it once a check has 100 or more samples. Disputed findings are reviewed by a person and the rule is fixed, not hidden.
