A purpose-built, deliberately vulnerable MCP (streamable HTTP) target for reap.
docker build -t reap-range .
docker run --rm -p 8080:8080 -p 8090:8090 reap-rangeTwo listeners come up:
| Target | Port | Paths | Posture |
|---|---|---|---|
bad |
8080 |
/mcp, /mcp/gated |
Deliberately insecure |
good |
8090 |
/mcp |
Correctly configured mirror |
The fully-open bad target — this is the one to GIF, it lights up a High/Medium/Info spread plus a confirmed handshake in a single scan:
reap -t http://localhost:8080/mcp --authorizedThe auth-gated bad target — shows reap reporting auth_state: auth-gated with an empty error list and exit 0, because an endpoint that correctly requires credentials is a recon result rather than a tool failure. Also the one check that only fires against a 401:
reap -t http://localhost:8080/mcp/gated --authorizedThe good target — proves the low false-positive rate. transport-plaintext is excluded here on purpose (see "Known limitation" below); everything else should be silent except one expected, benign info finding:
reap -t http://localhost:8090/mcp --authorized --exclude transport-plaintextTwo notes on where the OAuth checks look. mcp-oauth-bearer-challenge-missing reads WWW-Authenticate off the protected resource's own 401, not off the /.well-known/* response, a 200 metadata document has no reason to carry a challenge, and reading it there made the check fire on correctly-configured servers. mcp-oauth-metadata-posture now covers PKCE advertisement only.
The OAuth checks (mcp-oauth-metadata-posture, mcp-redirect-uri-laxity) try the RFC 9728 path-aware location (/.well-known/oauth-protected-resource/<path>) first and then fall back to the host root. This range only serves the root documents, so both checks still fire identically whether you point reap at bad/mcp or bad/mcp/gated. That's why "bad" and "good" OAuth postures live on separate ports rather than separate paths on one host.
tls-cert-health and transport-downgrade only evaluate https:// targets. A locally-trusted cert for a bare docker run demo would need a local CA (mkcert-style) or a TLS-terminating sidecar — and a self-signed cert would itself trip tls-cert-health's "certificate is self-signed" finding, so it can't demonstrate a clean result either way. Not built here; front this range with real TLS if you want to exercise those two checks.
Both report not-applicable rather than passing silently, so a scan of this range shows them in the probe tally as declined rather than clean.
Because the whole range is plain HTTP, transport-plaintext (ASI04, Medium) fires on every target, including the good one — that's expected, not a false positive on reap's part, and the good-target demo command above excludes it explicitly for that reason.
python3 server.py --bad-port 8080 --good-port 8090 [--good-allowed-host HOST:PORT ...]
Or via environment variables (used as defaults by the Docker image): BAD_PORT, GOOD_PORT, GOOD_ALLOWED_HOST (comma-separated). --good-allowed-host is only needed if you're reaching the good target through a hostname other than localhost/127.0.0.1 (e.g. a custom Docker network alias) — the good listener 400s on any Host header it doesn't recognize, which is the behavior mcp-host-header-validation is checking for.
