How it works
Verify, scan, report. In that order, every time.
mcpprobe behaves like a careful MCP client with a checklist. It proves it is allowed to talk to your server, asks only what it needs to, and writes down every request and response it relied on.
1. Check for an opt-out, then prove ownership
Before anything else, the scanner looks for an opt-out: a _mcpprobe DNS TXT record containing mcpprobe-opt-out=all, or a 200 from /.well-known/mcpprobe-opt-out. If either exists the scan stops with exit code 2. That check is the only request an opted-out host ever receives.
Then it needs proof that the person running the scan controls the target: a DNS TXT record they publish, or local verification for a stdio server running on their own machine. Hosted scans re-check that proof before every run, and it expires after 90 days. This is the same pattern established web scanners use, and it is what makes a scan an authorised test rather than an unsolicited one.
2. Fingerprint the protocol
MCP servers in the wild speak different revisions. The current specification (2026-07-28) is stateless, while most deployed servers still use the handshake-based 2025-11-25 or 2025-06-18 revisions, and some still expose the deprecated HTTP+SSE transport. The scanner tries each in turn and records which one answered, because several checks depend on it.
# protocol fingerprinting, in order
POST /mcp server/discover # 2026-07-28 (stateless)
POST /mcp initialize # 2025-11-25, then 2025-06-18
GET /mcp Accept: text/event-stream # legacy HTTP+SSE "endpoint" event3. Passive checks
16 of the 29 catalogue entries are passive checks, and they are what v0.1.0 runs. They send only what a normal client sends, plus a handful of benign malformed requests, never call a tool, and read what comes back: whether tools/list answers without a token, whether protected-resource metadata exists (RFC 9728), whether a forged Origin header is refused, how session ids look, and what the tool descriptions say. Tool descriptions are checked for instructions aimed at the model, references to other tools, hidden Unicode and secrets, and, with --baseline, their hashes are stored so the next scan can spot a silent change.
4. Active checks, coming soon, only inside your scope
8 checks will call your tools. In v0.1.0 they are registered stubs: every report lists them as skipped or not implemented, and no payloads are sent. The gate in front of them is already enforced: active mode is refused unless you pass --authorised-by, --scope and --verify, and every active run writes an authorisation record. When they ship, the payloads will be timing and echo canaries that prove an injection without reading, writing or deleting anything; tools annotated destructiveHint: true and tools passed to --exclude-tool will never be called. All requests are capped at 5 per second by default, 10 at most.
5. The report is the evidence
Each finding carries the exact request, the response, a severity, the OWASP items it maps to and the fix. Checks that rely on heuristics say so and show a confidence level. Output is a terminal summary, a JSON report and, if you ask, SARIF for GitHub code scanning. The exit code lets CI fail at the severity you choose.
6. Re-scan, and notice what changed
The GitHub Action runs the scan on every push, and hosted plans will re-run it on a schedule. Each run compares the tool-list hash with the previous one, so a description that changes after you approved it (a “rug pull”) shows up as a finding instead of going unnoticed. The badge reflects the latest result and goes grey after 30 days without a clean scan.
What is sent where
The CLI sends requests to the target you name, plus at most one GET to the authorization server it advertises (AUTH-03; turn it off with --no-follow-as). It sends no telemetry, nothing to us, and makes no LLM calls: the META-01 judge is a hook in the Python API that you connect to your own model, if you want one. Hosted scans run in AWS eu-west-2 from published IP addresses. Details are on the security page.