list_checks
List checks
List every check with its ID, severity, mode (active or passive) and availability.
Takes no parameters.

io.github.EnigmackerPublished by GitHub account Enigmackerv0.8.0Updated Nov 7, 2026
Active OAuth tests for remote MCP servers you own: tokens, PKCE, redirect_uri, audience, CSRF.
Runs active OAuth authorization tests against remote MCP servers you own, reporting results as JSON, SARIF or Markdown.
Desktop only via STDIO. STDIO servers start a local process, so they need the SourceWeft desktop host.
Follow the launch instructions in the repository.
[CI] [npm] [GitHub Marketplace] [License: MIT]
Active authorization testing for remote MCP servers you own.
Passive scanners read what a remote Model Context Protocol server announces: its OAuth metadata, its TLS setup, its tool descriptions. Some properties can only be confirmed by actually exercising the authorization server: does it really rotate refresh tokens, does it really refuse a redirect URI it never registered, does it enforce PKCE, does it accept a token meant for another server, does it slow down repeated failures?
mcp-authprobe runs those active checks, against servers whose ownership you have proven, and reports the results as JSON, SARIF and Markdown.
19 checks, field-tested against a certified OpenID provider: OAuth discovery, refresh token rotation and replay, redirect URI strictness, PKCE enforcement, single-use codes, token audience,
Originvalidation, tokens in URLs, session IDs used as credentials, CORS, throttling and consent CSRF (in a real browser). It works as a CLI with an interactivelogin, as an MCP server so an AI assistant can run the audit, and as a GitHub Action. A deliberately vulnerable lab lets you try everything in a minute.
Only test servers you own or are explicitly authorized to test. Active checks change state on the target. The tool enforces an ownership proof before running them, but responsibility stays with the operator.
[mcp-authprobe against its vulnerable lab: login, then a scan with 10 failing checks]
Besides its own test suite and lab, mcp-authprobe was run against real authorization stacks before each release:
Origin header. Every check that can be judged from outside passed; MAP-THR01 failed, as expected, since that provider leaves rate limiting to the deployment; and its consent page, protected by a per-flow identifier instead of an anti-CSRF field, is reported as inconclusive with the reason.redirect_uri not bound to the code, no Origin validation, unsafe redirect URIs accepted at registration, no refresh tokens, and crashes reported as inconclusive rather than passes. Its sessions correctly require the token on every request (MAP-SES01 passes).These runs led to several fixes: results kept when a server issues no refresh token or crashes during login, a throttling check that copes with a broken refresh grant, and the per-flow identifier analysis above.
Good tools already exist, and this one is meant to sit next to them, not replace them:
mcp-authprobe focuses on what only an active, authorized test can confirm, and uses check IDs (MAP-…) that do not clash with mcp-posture's (MCPP-…). mcp-posture reserves its own active IDs for this and points here; the two reports can be merged into one.
Requires Node.js 22.19 or later.
Releases are published from GitHub Actions with npm trusted publishing; check the provenance on the package page before installing a security tool.
From source:
mcp-authprobe lab starts a small MCP server with its own OAuth authorization server on 127.0.0.1, so you can see every check at work without touching a real server:
Log in with the account test / test and click Allow.
The lab only listens on 127.0.0.1 or ::1 (--host, --port, default 8787; names such as localhost are refused so that no resolver can move it elsewhere), keeps everything in memory, and stops with Ctrl-C. Its access tokens are unsigned JWTs, so MAP-AUD01 can read their audience. It is vulnerable on purpose: never expose it.
Without a valid proof the scan still runs discovery (the same read-only requests any MCP client sends before logging in), and every active check is reported as skipped.
For a local lab on your own machine:
Loopback targets with --allow-private do not need a proof.
Active checks register clients, rotate tokens and send failing requests. To make sure they only ever hit servers whose operator agreed to it, the scanner requires a proof for every host it would contact: the MCP server host and, when it differs, the authorization server host.
The proof is a hash, bound to the host, of a secret that only you hold:
Publish it in one of two places:
Design choices:
mcp.example.com is useless for any other host.MCP_AUTHPROBE_SECRET or --secret-file, never from the command line, so it does not land in shell history or process listings.You can remove the proof as soon as you are done testing.
MCP clients are usually public clients, so OAuth 2.1 requires their refresh tokens to be rotated, and RFC 9700 recommends revoking the whole token family when an old one is reused. These properties cannot be read from metadata; they have to be exercised.
The checks need a test grant: a client and a refresh token obtained by logging in to your own server with a dedicated test account.
The easy way is mcp-authprobe login (see Interactive login), which writes the file for you. You can also write it by hand, readable only by you (chmod 600):
token_endpoint_auth_method is none (default for public clients), client_secret_post (default when a secret is present) or client_secret_basic.
The scan sends at most three refresh_token requests to the token endpoint found by discovery:
Important:
MAP-RTK01 fails and the two replay checks are reported as not applicable.error (inconclusive), never as a pass.login behaves like a careful native MCP client (RFC 8252):
127.0.0.1 and a random port.--no-browser). Log in with your test account.GET /callback carrying the expected state, and echoes nothing back.state, the RFC 8707 resource, and checks the RFC 9207 iss parameter against the discovered issuer.--force. Tokens are never printed.login itself does not require an ownership proof: it only does what any MCP client does when you connect it (one registration, your own browser login, one code exchange). Only the extra login checks below require the proof.Seven checks are observed during login, because they need a fresh authorization code or a valid access token. They run only when ownership of every contacted host is proven (--no-probe skips them), in this order:
The results are stored in the grant file and reported by the next scan of the same target. The refresh token kept in the grant is always the newest one, so rotation during these probes does not break the scan. A network failure during a probe makes that check inconclusive; it never loses the grant or the other results.
If a probe obtains tokens (the check fails), the code is spent: the following pre-exchange probes are reported as not observed, the tokens obtained are kept as the grant, and MAP-COD01 still runs (presenting the code again is its second use).
Some strict servers invalidate a code after any failed attempt. That is acceptable behaviour: only the first refusal is trusted and the following probes are reported as inconclusive. Likewise, a server that revokes the tokens after a code reuse (recommended) passes MAP-COD01. In both cases login writes the results without a grant; log in again with --no-probe --force --out <same file> to add a grant: the earlier results of the same target are kept.
If an authorization server redirects to a URI the client never registered, authorization codes can end up with an attacker. OAuth 2.1 requires exact string matching. These checks need dynamic client registration on your server; no test grant and no login are required.
How they work:
https://client.mcp-authprobe.invalid/callback.state, RFC 8707 resource), then one request per variant, and compares the answers. Redirects are never followed.mcp-authprobe test client (safe to delete).Safety by design: every host used lives under the reserved .invalid top-level domain (RFC 2606), which can never resolve. Even a vulnerable server could not deliver a code anywhere.
Reading the results: a variant answered exactly like the baseline (for example both redirected to the login page) is reported as inconclusive (error), never as a pass, because some servers only check redirect_uri after the user logs in. MAP-RDR03 (token endpoint enforcement) is observed during interactive login.
Without rate limiting, refresh tokens, authorization codes and client secrets can be guessed at line speed. MAP-THR01 checks that the token endpoint slows down repeated failures, with strict bounds:
--throttle-attempts, between 5 and 20, hard-capped), one at a time.unsupported_grant_type (a server without a working refresh grant), the check switches once to random authorization codes and starts counting again.Retry-After header, or an OAuth slow_down / temporarily_unavailable error.A failure means no throttling was seen within the cap. Your limit may simply be higher than 20; the finding asks you to confirm that one exists.
If a third-party website can submit your consent page on behalf of a logged-in user, it can make them grant access without noticing (the confused deputy described in the MCP security best practices). MAP-CSR01 checks that the consent decision is protected, in a real browser with your test account:
--browser msedge, or --browser-path <executable>) on the authorization page. No browser is downloaded; playwright-core is an optional dependency used only by this command.Many authorization servers (node-oidc-provider, Ory Hydra, Keycloak) have no anti-CSRF field: the consent form carries a per-flow identifier issued by the server, in a hidden field or in the form's URL (/interaction/<id>). When there is no anti-CSRF field but such an identifier, two extra submissions come first, before anything can be granted: one with the identifier altered and one without any cookie, both from a foreign Origin. If either is accepted, the check fails. If both are refused and the untouched form is accepted, the result is inconclusive with an explanation: this design is safe when the identifier is bound to the user's own session, which cannot be proven from a single session.
The check fails when consent is granted without a valid anti-CSRF token. It only passes when the control proves that the tool reproduces a real grant; otherwise (labels it cannot recognise, consent sent by JavaScript, multi-step flows, single-use tokens) the result is inconclusive, never a pass. The finding also reports whether the session cookies are explicitly SameSite=Lax/Strict, which blocks the attack from other sites in modern browsers, but not from same-site subdomains or older clients; a cookie without the attribute is reported as Unspecified. Ctrl-C, closing the browser or a 10-minute timeout stop the check cleanly and still delete the temporary client.
It is not available in MCP server mode, because it needs you to log in.
mcp-authprobe mcp runs the same engine as an MCP server over stdio, so an AI assistant in any MCP client can audit your servers for you.
Add it to your client's MCP configuration (most clients use this mcpServers format):
The server is also listed in the official MCP Registry as io.github.Enigmacker/mcp-authprobe, so registry-aware clients can install it directly.
From a source checkout, use "command": "node" with "args": ["/path/to/mcp-authprobe/dist/cli.js", "mcp"].
The server is built so that the assistant, or a prompt injection reaching it, cannot widen what the scanner may do:
scan tool takes a URL and nothing else.scan is annotated as destructive, so well-behaved clients ask for your confirmation before each call. It consumes the test grant and registers temporary clients.https targets are accepted; plain http only for loopback or private-address labs with MCP_AUTHPROBE_ALLOW_PRIVATE=1. URLs with embedded credentials are refused, and only one scan runs at a time.MCP_AUTHPROBE_LAB_ORIGINS. An assistant cannot point active requests at arbitrary local services.login do), so it cannot be spent on another server.The repository's Dockerfile builds the same stdio MCP server:
In a container, --allow-private labs on your machine are not reachable as 127.0.0.1; use the npm package for the lab.
mcp-authprobe checks list prints the catalogue, and mcp-authprobe checks show <ID> explains a check: why it matters, how to fix it, and references. IDs are stable and never reused.
Families: DSC discovery, RTK refresh tokens, RDR redirect URIs, THR throttling, CSR consent CSRF, PKC PKCE, COD authorization codes, AUD token audience, ORG Origin validation, TOK token transport, SES sessions, COR CORS.
MAP-ORG01 runs in every scan without an ownership proof: it sends two ordinary initialize requests, one with a foreign Origin. Most servers ask for a token before looking at Origin; the scan then reports it as inconclusive, and login observes it with a valid token. MAP-COR01 also runs in every scan: a CORS preflight and an ordinary request with a foreign Origin; it fails only when that origin is reflected together with Access-Control-Allow-Credentials: true.
--only and --skip take comma-separated check IDs (MAP-RDR01, or just RDR01) or families (RDR), case-insensitive. --skip wins over --only; an unknown name is a usage error, so a typo never silently changes what runs.
A check that is not selected sends no request and is listed as skipped (not selected), so a report never suggests it passed. The three refresh token checks are one sequence: selecting any of them runs the three (the others are then reported as not selected), and skipping all three leaves the test grant untouched. The MCP server's scan tool takes the same only and skip arguments.
mcp-posture reads what a server announces; mcp-authprobe tests what it enforces. Run mcp-posture first, then hand its JSON report to the scan:
The report gains a passive section with mcp-posture's findings (with their stable fingerprints) and one line per subject both tools look at:
The report must be mcp-posture's JSON (schema_version 1.x) and contain a result for the same URL. It is treated as untrusted input: size-capped, type-checked and neutralized like everything else. Suppressed mcp-posture findings are left out, and mcp-posture's findings never change mcp-authprobe's exit code. The merged results appear in the table, JSON (a posture block) and Markdown reports; SARIF keeps mcp-authprobe's results only, since mcp-posture writes its own.
All server-controlled text is neutralized before it reaches a report: terminal escape sequences, bidi overrides and invisible characters are escaped, and token-shaped values are redacted.
Run the scan on every deployment, or every night, so a change that reintroduces a flaw is caught before anyone else finds it.
The results table is added to the job summary. The step fails when a check fails at or above fail-on.
Both tools have an Action, so the passive and active views fit in one job:
Outputs: exit-code, failed (number of failing checks), json-file, sarif-file, markdown-file.
The secret is passed through the environment, never on a command line. The refresh token checks and the login checks are not run by the action: they need a test grant from an interactive login, and a grant is consumed by a scan. Run them by hand after a change to your authorization server.
Run active scans against a staging environment rather than production whenever you can.
Without the action, the same thing in plain steps:
The scanner talks to servers that may be hostile, and it may be driven by an AI agent. It protects both you and third parties:
login, which behaves like an ordinary MCP client; see above).--allow-private, so tokens and secrets never travel in cleartext over the internet.Retry-After) and server errors (5xx) make a check report error, never pass.--allow-private is passed.0.1.0
MAP-RDR030.2.0
0.3.0
lab)0.4.0
--only / --skip0.5.0
0.6.0
0.7.0
0.8.0
Next
Tests never leave the machine: integration tests run against a fixture server bound to 127.0.0.1.
MIT © 2026 Enigmacker
Source: README.md at commit e685e40
List checks
List every check with its ID, severity, mode (active or passive) and availability.
Takes no parameters.
Explain a check
Explain one check: why it matters, how to fix a failure, and references.
idrequiredOwnership proof to publish
Return the ownership proof the user must publish for a host (well-known file or DNS TXT record) before active tests can run. The secret itself is never returned.
hostrequiredVerify ownership proof
Check whether the ownership proof for a host is published and matches the configured secret.
hostrequiredScan a remote MCP server
Run discovery and, on hosts with a valid ownership proof, the active authorization tests against a remote MCP server URL. Active tests register and delete temporary OAuth clients and CONSUME the configured test grant. Only use on servers the user owns or is authorized to test, after the user confirmed.
urlrequiredonlyskip90% of the applicable checks met
How complete this listing is, computed from the registry entry, the endpoint and public sources. Not a rating of the server and not a statement that it is safe or working today: reachability is a dated check, and no installs or reviews go into the score.
Declare the credential headers in the registry entry, or keep the endpoint answering the probe so its auth can be classified.
# run without installingnpx mcp-authprobe --help
# or install globallynpm install -g mcp-authprobegit clone https://github.com/Enigmacker/mcp-authprobe.gitcd mcp-authprobenpm cinpm run buildnode dist/cli.js --help# Terminal 1: a deliberately vulnerable server (or --profile secure)npx mcp-authprobe lab
# Terminal 2npx mcp-authprobe scan http://127.0.0.1:8787/mcp --allow-privatenpx mcp-authprobe login http://127.0.0.1:8787/mcp --allow-private --out lab-grant.jsonnpx mcp-authprobe scan http://127.0.0.1:8787/mcp --allow-private --credentials-file lab-grant.json# 1. Create a secret and the proof to publish for your MCP hostmcp-authprobe ownership init mcp.example.com
# 2. Publish the proof (well-known file or DNS TXT), then keep the secret in your environmentexport MCP_AUTHPROBE_SECRET='…'
# 3. Check the proof is visiblemcp-authprobe ownership verify mcp.example.com
# 4. Scanmcp-authprobe scan https://mcp.example.com/mcp --sarif results.sarif --markdown summary.md
# 5. Log in once with a dedicated test account to create a test grantmcp-authprobe login https://mcp.example.com/mcp --out ./test-grant.json
# 6. Full scan, including the refresh token checksmcp-authprobe scan https://mcp.example.com/mcp --credentials-file ./test-grant.jsonmcp-authprobe scan http://127.0.0.1:8080/mcp --allow-privatemcp-authprobe-verification=sha256:<hex(sha256("<host>:<secret>"))>{ "client_id": "your-test-client-id", "refresh_token": "the-refresh-token", "client_secret": "only-for-confidential-clients", "token_endpoint_auth_method": "none"}mcp-authprobe login https://mcp.example.com/mcp --out ./test-grant.jsonnpx -p mcp-authprobe -p playwright-core mcp-authprobe consent-check https://mcp.example.com/mcp{ "mcpServers": { "mcp-authprobe": { "command": "npx", "args": ["-y", "mcp-authprobe", "mcp"], "env": { "MCP_AUTHPROBE_SECRET_FILE": "/home/you/.config/mcp-authprobe/secret", "MCP_AUTHPROBE_CREDENTIALS_FILE": "/home/you/.config/mcp-authprobe/test-grant.json" } } }}docker build -t mcp-authprobe https://github.com/Enigmacker/mcp-authprobe.gitdocker run -i --rm -e MCP_AUTHPROBE_SECRET mcp-authprobemcp-authprobe scan https://mcp.example.com/mcp --skip THR # production: no failed token requestsmcp-authprobe scan https://mcp.example.com/mcp --only RDR02 # re-check one fixmcp-posture scan https://mcp.example.com/mcp --json posture.jsonmcp-authprobe scan https://mcp.example.com/mcp --posture-report posture.json --markdown report.mdname: MCP authorizationon: schedule: - cron: "17 3 * * *" workflow_dispatch:
permissions: contents: read security-events: write # only for upload-sarif
jobs: scan: runs-on: ubuntu-latest steps: - uses: Enigmacker/[email protected] with: url: https://staging.example.com/mcp secret: ${{ secrets.MCP_AUTHPROBE_SECRET }} skip: THR # optional: no failed token requests upload-sarif: true # optional: findings in the Security tab - uses: batou9150/[email protected] with: targets: https://staging.example.com/mcp args: --json posture.json fail: false # let the active scan run too; each tool reports its own findings - uses: Enigmacker/[email protected] with: url: https://staging.example.com/mcp secret: ${{ secrets.MCP_AUTHPROBE_SECRET }} posture-report: posture.json- uses: actions/setup-node@v7 with: node-version: 22- run: npx mcp-authprobe scan "$MCP_URL" --sarif results.sarif --markdown summary.md env: MCP_URL: https://mcp.example.com/mcp MCP_AUTHPROBE_SECRET: ${{ secrets.MCP_AUTHPROBE_SECRET }}- run: cat summary.md >> "$GITHUB_STEP_SUMMARY" if: always()npm cinpm run typechecknpm test # unit and integration tests against a local fixture servernpm run buildnpm run check # all of the abovenpm run demo # re-record docs/demo/demo.gif (needs Chrome and ffmpeg)