Docs · v0.1.0
Quickstart
Sixty seconds from install to a report. 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.
# 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"$ 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 testtransport=http | revision=2025-11-25 (handshake) | auth=none | server=fake-mcp 0.0.1 | tools=7Findings 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 ownershipSUPPLY-01 active skipped not applicable to http transport38 request/response pairs recorded as evidence.Result: FAIL (findings at or above high)
29 checks in the catalogue: 16 passive checks implemented in v0.1.0, 8 active checks coming soon, 5 attestation-only governance items. The same page is at /docs.md; the scanner's own README is in the repository.
Passive vs active
| Aspect | 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 unless you pass --authorised-by, --scope and a verification method. Every active run writes an authorisation record (scan.authorisation.json next to the report) with who authorised it, the scope, the verification proof, the rate cap and excluded tools.
| 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 |
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.jsonCommands and flags
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
- 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
mcpprobe https://your-server.example/mcp --baseline .mcpprobe/baseline.jsonThe first run stores canonical SHA-256 hashes of every tool, prompt, resource and the server instructions. Later runs report anything added, removed or changed (META-02) and update the file; use --no-write-baseline to compare without updating.
Reports and evidence
--json FILE: the full report, including every request and response (timestamp, method, status, a header subset, bodies truncated to 4 KB).Authorizationvalues and recognised secret patterns are redacted.--sarif FILE: SARIF 2.1.0.-vadds descriptions and scan notes to the terminal;-qprints nothing.- Server-supplied text is shown with control characters made visible and is never interpreted as terminal markup.
GitHub Action
A Docker container action, so no Python setup is needed. Upload the SARIF to code scanning so findings appear as alerts on the pull request.
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. What the badge proves.
[](https://mcpprobe.tech/r/your-server.example)API and MCP endpoint
The status endpoint is public and needs no key. It returns the same state the badge shows, so registries and directories can display it without scraping.
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}.svgreturns the badge: passing, failing, stale or unknown.GET /scanning-ips.jsonlists the addresses hosted scans come from.- An MCP endpoint that lets agents read reports is planned; it is not live in v0.1.0.
The scanner is also a Python library, with an optional LLM-judge hook for META-01. mcpprobe makes no LLM calls itself.
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.
Every check's probe, evidence, false positives and fix are in the checks catalogue; how scans are authorised is in the scanning policy.