mcp-posture
Security posture scanner for remote MCP servers. Point it at a Streamable HTTP endpoint and it audits the OAuth setup (MCP Authorization spec, RFC 9728, RFC 8414, PKCE, RFC 8707, Client ID Metadata Documents), transport hardening and the tool surface (poisoning, shadowing, rug pulls), with spec-cited findings and CI-native output (SARIF, JSON, Markdown).
Only scan servers you own or are authorized to test.
Why
Most MCP scanners inspect local client configs and stdio servers. mcp-posture focuses on
remote servers and what an attacker sees from the outside before logging in: how the server
challenges, which authorization server it trusts, whether that server enforces PKCE S256, whether
tokens are audience-bound, whether the tool descriptions hide instructions. Every finding cites the
spec section and the MCP revision it applies to (2025-03-26 through 2026-07-28).
Quickstart
# no install needed (or: pipx install mcp-posture / uv tool install mcp-posture)
uvx mcp-posture scan https://mcp.example.com/mcp
# machine-readable outputs, fail the build on high or worse
mcp-posture scan https://mcp.example.com/mcp --sarif results.sarif --markdown summary.md --fail-on high
# lint your own client's Client ID Metadata Document
mcp-posture cimd lint https://app.example.com/oauth/client.json
# scan every remote server declared in your MCP clients (.mcp.json, Claude, VS Code, Cursor, Windsurf)
mcp-posture discover
mcp-posture scan --from-client-config auto
# list tools behind authentication: log in to your own server in the browser (token never shown)
mcp-posture scan https://mcp.example.com/mcp --login
# ... or pass a token through the environment, never on the command line
MCP_TOKEN=... mcp-posture scan https://mcp.example.com/mcp --token-env MCP_TOKEN
Docker (distroless, nonroot, signed with cosign):
docker run --rm ghcr.io/batou9150/mcp-posture scan https://mcp.example.com/mcp
GitHub Action (SARIF to code scanning, Markdown to the job summary):
- uses: batou9150/mcp-posture@main # pin a tag or SHA
with:
targets-file: mcp-servers.txt
fail-on: high
Claude Code skill: the scanner plus a semantic review of tool descriptions, prioritization and remediation snippets for common authorization servers and gateways.
/plugin marketplace add batou9150/mcp-posture
/plugin install mcp-posture@mcp-posture
Then ask "audit the security of https://mcp.example.com/mcp".
Sample output (local misconfigured fixture, scripts/demo_servers.py):
http://127.0.0.1:8402/mcp
spec 2025-06-18 (negotiated) · transport streamable-http · auth not required
critical MCPP-ASM04 code_challenge_methods_supported ['plain'] does not include S256.
high MCPP-ASM02 Metadata declares issuer 'http://127.0.0.1:8402/', expected 'http://127.0.0.1:8402'.
high MCPP-AUTHN01 tools/list answered without credentials (2 tool(s) including delete_note).
high MCPP-PRM06 bearer_methods_supported includes `query`.
high MCPP-TRN08 Session IDs look sequential (numerically close across two sessions).
medium MCPP-PRM03 resource '.../mcp/' does not match '.../mcp' (trailing slash only).
...
What it checks
| Family | Checks | Covers |
|---|---|---|
TRN |
11 | HTTPS, TLS version and certificate, HTTP→HTTPS, HSTS, legacy SSE, session IDs in URLs, Mcp-Session-Id entropy, metadata headers, endpoint redirects |
AUTHN |
6 | Anonymous tools/list, Bearer challenge, resource_metadata discovery, error leakage |
PRM |
11 | RFC 9728 Protected Resource Metadata: presence, resource exact match, authorization servers, query tokens, scopes, signed metadata |
ASM |
13 | RFC 8414 / OIDC metadata: issuer match, HTTPS endpoints, PKCE S256 / plain, implicit and password grants, DCR, iss (RFC 9207) |
CIMD |
10 | Client ID Metadata Document support and registration strategy; cimd lint rules for your own client's document |
SCP |
3 | Over-broad scopes, missing scope in challenges, PRM/AS scope consistency |
TOOL |
10 | Instruction-like text, invisible/bidi/tag Unicode, encoded blobs, shadowing, secret paths and exfil URLs, annotations, unconstrained URL/path/code inputs, confusable names, nesting too deep to inspect |
PIN |
4 | Rug pulls: tools/prompts/resources added, removed or changed since mcp-posture pin |
mcp-posture checks list prints the catalogue; mcp-posture checks show MCPP-ASM04 explains one
check (rationale, remediation, references). Check IDs are stable and never reused.
The research behind the catalogue, with every MUST/SHOULD mapped to a check, is in
docs/spec-notes.md.
The default mode is passive: metadata GETs plus the standard MCP handshake and list calls.
No tool is ever called. Behaviours that only an active probe could confirm (Origin validation,
token audience enforcement, PKCE enforcement) are listed as not verified in
docs/spec-notes.md.
How it works
flowchart LR
T[Targets<br/>URLs · targets file · client configs] --> C[Collect<br/>hardened HTTP client]
C --> P[MCP prober<br/>server/discover → initialize → SSE]
C --> D[OAuth discovery<br/>401 challenge · PRM · AS metadata · TLS]
P & D --> X[Immutable scan context]
X --> K[Checks<br/>pure functions, registry]
K --> S[Suppressions · baseline]
S --> R[Reports<br/>table · JSON · SARIF · Markdown]
Each target is collected once (all network I/O, through a client that refuses private addresses
at connect time), frozen into a context, then every check runs as a pure function over it. A
failing check becomes an MCPP-ERR00 finding instead of aborting the scan.
CI usage
# mcp-posture.toml (CLI flags override it)
[scan]
targets_file = "mcp-servers.txt"
fail_on = "high"
baseline = "mcp-posture.lock.json" # rug-pull detection, created with `mcp-posture pin`
disable = ["ASM08"]
# .mcp-posture-ignore: every suppression needs a justification; expired ones resurface
[[ignore]]
check = "MCPP-ASM09"
target = "https://mcp.example.com/*"
justification = "Open DCR is intended: public client registry"
expires = 2026-12-31
SARIF results are anchored to the line of the file that declares each target (targets file,
mcp-posture.toml, .mcp.json), so GitHub code scanning can display them.
| Exit code | Meaning |
|---|---|
0 |
No finding at or above --fail-on, every target reachable |
1 |
At least one unsuppressed finding at or above --fail-on |
2 |
Usage or configuration error |
3 |
A target was unreachable (and no blocking finding) |
Safety of the scanner
- Tokens are read only from an env var, a file or stdin, and are redacted from every output and log.
- Private, loopback, link-local and cloud-metadata addresses are refused at connect time (after DNS
resolution, every redirect hop included) unless
--allow-privateis passed. - Credentials are never forwarded across origins; response size and time are capped.
- Server-controlled text is neutralized in reports: no raw control, bidi or invisible characters reach your terminal or PR comments.
- No telemetry.
Compared with other MCP scanners
Several good tools exist; they solve different problems. Facts as of 2026-10 (see
docs/spec-notes.md for the survey):
| mcp-posture | Snyk agent-scan | Cisco mcp-scanner | Ramparts | MCPJam OAuth conformance | |
|---|---|---|---|---|---|
| Focus | remote server posture | local agent configs and tools | configs, stdio and remote servers | servers, configs, skills | live OAuth flow conformance |
| OAuth / PRM / AS metadata audit (pre-login) | yes, per RFC, per MCP revision | no | no | no | yes (needs a login) |
| Tool poisoning analysis | regex heuristics (+ semantic review via the skill) | remote API analysis | YARA + LLM | YARA + LLM | no |
| Rug-pull pinning | yes | signatures | no | yes | no |
| SARIF | yes | no | no | yes | no (JUnit) |
Pair mcp-posture with a runtime proxy (e.g. mcp-context-protector)
for enforcement; it reports, it does not block.
Responsible use
Scan only servers you own or have written permission to test. Passive mode sends a handful of standard requests; even so, unsolicited scanning of third-party infrastructure may violate their terms or the law.
Contributing and security
See CONTRIBUTING.md (how to add a check) and SECURITY.md (reporting vulnerabilities, verifying release signatures and attestations).
Development
uv sync
uv run ruff check && uv run ruff format --check && uv run mypy && uv run pytest
uv run python scripts/demo_servers.py # local fixtures to scan with --allow-private
uv run --group docs mkdocs serve # docs site
License
MIT © Baptiste PIRAULT
Metadata
Release files for mcp-posture 1.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| mcp_posture-1.1.1.tar.gz | 295.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mcp_posture-1.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 405.5 kB
Release files / mcp_posture-1.1.1.tar.gz
| Download URL | mcp_posture-1.1.1.tar.gz |
|---|---|
| Size | 295.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
69af2bcaa67ca8d579011d8e119a681c3c068dcd41523548d585bd84c3444a7b
|
|
BLAKE2b-256 checksum How to use checksums |
480d9519851499fec35fb7c32211008e89be4b4fb661ada42b4cce90a68f2460
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.
Transparency logRelease files / mcp_posture-1.1.1-py3-none-any.whl
| Download URL | mcp_posture-1.1.1-py3-none-any.whl |
|---|---|
| Size | 109.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
28beb97b25bdad0fe37cad4330674291128194dc4dbf5cb21d8a1fc847a006b0
|
|
BLAKE2b-256 checksum How to use checksums |
3496790bbdbc4a7572ca9075c45cf4f19585c260f5f9a703d4dcf94b4996c118
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.
Transparency log