mcp-posture 1.0.0rc1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. mcp_posture-1.0.0rc1/.gitignore +136 -0
  2. mcp_posture-1.0.0rc1/CHANGELOG.md +25 -0
  3. mcp_posture-1.0.0rc1/CONTRIBUTING.md +72 -0
  4. mcp_posture-1.0.0rc1/LICENSE +21 -0
  5. mcp_posture-1.0.0rc1/PKG-INFO +229 -0
  6. mcp_posture-1.0.0rc1/README.md +202 -0
  7. mcp_posture-1.0.0rc1/SECURITY.md +55 -0
  8. mcp_posture-1.0.0rc1/docs/schema/report-v1.json +406 -0
  9. mcp_posture-1.0.0rc1/pyproject.toml +111 -0
  10. mcp_posture-1.0.0rc1/scripts/__init__.py +0 -0
  11. mcp_posture-1.0.0rc1/scripts/check_version.py +61 -0
  12. mcp_posture-1.0.0rc1/scripts/demo_servers.py +103 -0
  13. mcp_posture-1.0.0rc1/src/mcp_posture/__init__.py +3 -0
  14. mcp_posture-1.0.0rc1/src/mcp_posture/__main__.py +5 -0
  15. mcp_posture-1.0.0rc1/src/mcp_posture/checks/__init__.py +5 -0
  16. mcp_posture-1.0.0rc1/src/mcp_posture/checks/_refs.py +85 -0
  17. mcp_posture-1.0.0rc1/src/mcp_posture/checks/_util.py +60 -0
  18. mcp_posture-1.0.0rc1/src/mcp_posture/checks/asm.py +391 -0
  19. mcp_posture-1.0.0rc1/src/mcp_posture/checks/authn.py +263 -0
  20. mcp_posture-1.0.0rc1/src/mcp_posture/checks/cimd.py +70 -0
  21. mcp_posture-1.0.0rc1/src/mcp_posture/checks/pin.py +107 -0
  22. mcp_posture-1.0.0rc1/src/mcp_posture/checks/prm.py +356 -0
  23. mcp_posture-1.0.0rc1/src/mcp_posture/checks/scp.py +116 -0
  24. mcp_posture-1.0.0rc1/src/mcp_posture/checks/tool.py +459 -0
  25. mcp_posture-1.0.0rc1/src/mcp_posture/checks/trn.py +406 -0
  26. mcp_posture-1.0.0rc1/src/mcp_posture/cimd_lint.py +449 -0
  27. mcp_posture-1.0.0rc1/src/mcp_posture/cli.py +582 -0
  28. mcp_posture-1.0.0rc1/src/mcp_posture/client.py +383 -0
  29. mcp_posture-1.0.0rc1/src/mcp_posture/config.py +115 -0
  30. mcp_posture-1.0.0rc1/src/mcp_posture/context.py +193 -0
  31. mcp_posture-1.0.0rc1/src/mcp_posture/discovery.py +259 -0
  32. mcp_posture-1.0.0rc1/src/mcp_posture/docs.py +99 -0
  33. mcp_posture-1.0.0rc1/src/mcp_posture/engine.py +311 -0
  34. mcp_posture-1.0.0rc1/src/mcp_posture/heuristics.py +305 -0
  35. mcp_posture-1.0.0rc1/src/mcp_posture/models.py +212 -0
  36. mcp_posture-1.0.0rc1/src/mcp_posture/net.py +597 -0
  37. mcp_posture-1.0.0rc1/src/mcp_posture/pin.py +111 -0
  38. mcp_posture-1.0.0rc1/src/mcp_posture/py.typed +0 -0
  39. mcp_posture-1.0.0rc1/src/mcp_posture/redact.py +117 -0
  40. mcp_posture-1.0.0rc1/src/mcp_posture/registry.py +98 -0
  41. mcp_posture-1.0.0rc1/src/mcp_posture/report/__init__.py +64 -0
  42. mcp_posture-1.0.0rc1/src/mcp_posture/report/json.py +24 -0
  43. mcp_posture-1.0.0rc1/src/mcp_posture/report/markdown.py +101 -0
  44. mcp_posture-1.0.0rc1/src/mcp_posture/report/sarif.py +172 -0
  45. mcp_posture-1.0.0rc1/src/mcp_posture/report/table.py +74 -0
  46. mcp_posture-1.0.0rc1/src/mcp_posture/sources.py +180 -0
  47. mcp_posture-1.0.0rc1/src/mcp_posture/suppress.py +117 -0
  48. mcp_posture-1.0.0rc1/src/mcp_posture/surface.py +90 -0
  49. mcp_posture-1.0.0rc1/tests/__init__.py +0 -0
  50. mcp_posture-1.0.0rc1/tests/conftest.py +65 -0
  51. mcp_posture-1.0.0rc1/tests/fixtures/__init__.py +0 -0
  52. mcp_posture-1.0.0rc1/tests/fixtures/servers.py +449 -0
  53. mcp_posture-1.0.0rc1/tests/golden/report.json +181 -0
  54. mcp_posture-1.0.0rc1/tests/golden/report.md +27 -0
  55. mcp_posture-1.0.0rc1/tests/golden/report.sarif +2569 -0
  56. mcp_posture-1.0.0rc1/tests/golden/report.txt +26 -0
  57. mcp_posture-1.0.0rc1/tests/schemas/sarif-schema-2.1.0.json +3389 -0
  58. mcp_posture-1.0.0rc1/tests/test_action.py +115 -0
  59. mcp_posture-1.0.0rc1/tests/test_catalogue.py +68 -0
  60. mcp_posture-1.0.0rc1/tests/test_checks_asm.py +240 -0
  61. mcp_posture-1.0.0rc1/tests/test_checks_authn.py +106 -0
  62. mcp_posture-1.0.0rc1/tests/test_checks_cimd.py +39 -0
  63. mcp_posture-1.0.0rc1/tests/test_checks_pin.py +91 -0
  64. mcp_posture-1.0.0rc1/tests/test_checks_prm.py +185 -0
  65. mcp_posture-1.0.0rc1/tests/test_checks_scp.py +64 -0
  66. mcp_posture-1.0.0rc1/tests/test_checks_tool.py +233 -0
  67. mcp_posture-1.0.0rc1/tests/test_checks_trn.py +257 -0
  68. mcp_posture-1.0.0rc1/tests/test_cimd_lint.py +244 -0
  69. mcp_posture-1.0.0rc1/tests/test_cli.py +301 -0
  70. mcp_posture-1.0.0rc1/tests/test_client.py +147 -0
  71. mcp_posture-1.0.0rc1/tests/test_discovery.py +111 -0
  72. mcp_posture-1.0.0rc1/tests/test_docs.py +39 -0
  73. mcp_posture-1.0.0rc1/tests/test_engine.py +143 -0
  74. mcp_posture-1.0.0rc1/tests/test_fuzz.py +317 -0
  75. mcp_posture-1.0.0rc1/tests/test_heuristics.py +113 -0
  76. mcp_posture-1.0.0rc1/tests/test_net.py +278 -0
  77. mcp_posture-1.0.0rc1/tests/test_redact.py +98 -0
  78. mcp_posture-1.0.0rc1/tests/test_report.py +318 -0
  79. mcp_posture-1.0.0rc1/tests/test_suppress_config_sources.py +221 -0
  80. mcp_posture-1.0.0rc1/uv.lock +1970 -0
@@ -0,0 +1,136 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ target/
76
+
77
+ # Jupyter Notebook
78
+ .ipynb_checkpoints
79
+
80
+ # IPython
81
+ profile_default/
82
+ ipython_config.py
83
+
84
+ # pyenv
85
+ .python-version
86
+
87
+ # pipenv
88
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
90
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
91
+ # install all needed dependencies.
92
+ #Pipfile.lock
93
+
94
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95
+ __pypackages__/
96
+
97
+ # Celery stuff
98
+ celerybeat-schedule
99
+ celerybeat.pid
100
+
101
+ # SageMath parsed files
102
+ *.sage.py
103
+
104
+ # Environments
105
+ .env
106
+ .venv
107
+ env/
108
+ venv/
109
+ ENV/
110
+ env.bak/
111
+ venv.bak/
112
+
113
+ # Spyder project settings
114
+ .spyderproject
115
+ .spyproject
116
+
117
+ # Rope project settings
118
+ .ropeproject
119
+
120
+ # mkdocs documentation
121
+ /site
122
+
123
+ # mypy
124
+ .mypy_cache/
125
+ .dmypy.json
126
+ dmypy.json
127
+
128
+ # Pyre type checker
129
+ .pyre/
130
+
131
+ # intellij
132
+ .idea
133
+
134
+
135
+ # mcp-posture
136
+ .mcp-posture-cache/
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ ## [1.0.0-rc1](https://github.com/batou9150/mcp-posture/releases/tag/v1.0.0-rc1) (2026-10-05)
4
+
5
+ First release candidate.
6
+
7
+ ### Features
8
+
9
+ * Passive scanner for remote MCP servers (Streamable HTTP, legacy HTTP+SSE detection), aware of
10
+ MCP revisions 2025-03-26, 2025-06-18, 2025-11-25 and 2026-07-28.
11
+ * Checks: transport (TRN01-11), authentication challenge (AUTHN01-06), Protected Resource
12
+ Metadata (PRM01-11), authorization server metadata (ASM01-13), Client ID Metadata Documents
13
+ (CIMD01-02), scopes (SCP01, SCP02, SCP04), tool surface (TOOL01-10), rug-pull pinning
14
+ (PIN01-04).
15
+ * `mcp-posture cimd lint` for your own client's metadata document (CIMD50-57).
16
+ * Reports: table, JSON (versioned schema), SARIF 2.1.0 anchored to the declaring file,
17
+ Markdown; suppressions with justification and expiry; `mcp-posture.toml`; targets from MCP
18
+ client configs.
19
+ * Hardened HTTP layer: connect-time SSRF guard (IPv6 forms embedding IPv4 included, also
20
+ enforced with `--proxy`), manual redirects without cross-origin credentials, capped bodies,
21
+ a deadline on response headers and a per-target time limit (`--target-timeout`), token
22
+ redaction that also masks truncated fragments, bounded JSON nesting.
23
+ * Reports neutralize server-controlled text: no control, bidi or invisible characters in any
24
+ format, and no links, images, mentions or fence breaks in the Markdown summary.
25
+ * GitHub Action, distroless container image, Claude Code skill, documentation site.
@@ -0,0 +1,72 @@
1
+ # Contributing
2
+
3
+ Thanks for helping. Bug reports, false positives and new checks are all welcome.
4
+
5
+ ## Ground rules
6
+
7
+ - Only test against servers you own. Tests run against in-process fixtures
8
+ (`tests/fixtures/servers.py`) or `scripts/demo_servers.py` on localhost; never add a test,
9
+ fixture or example that contacts a third-party server.
10
+ - No real hostnames, tokens, tool definitions or reports from production systems in issues,
11
+ fixtures or docs. Use `example.com` / `*.test` names and synthetic data.
12
+ - The scanner is passive: metadata `GET`s, the MCP handshake and list calls. Changes that make
13
+ it call tools or send state-changing requests need a design discussion first.
14
+
15
+ ## Development
16
+
17
+ ```bash
18
+ uv sync
19
+ uv run ruff check && uv run ruff format --check && uv run mypy && uv run pytest
20
+ ```
21
+
22
+ `pytest` enforces 90% coverage. `pre-commit install` runs the same checks on commit.
23
+ `uv run python scripts/demo_servers.py` starts local fixture servers (secure on :8401,
24
+ misconfigured on :8402, poisoned on :8403) to scan with `--allow-private`.
25
+
26
+ ## Adding a check
27
+
28
+ 1. **Find the requirement.** Every check maps to a normative statement (MCP spec revision, RFC,
29
+ IETF draft or security best practice). Add or update its row in `docs/spec-notes.md`.
30
+ 2. **Pick an ID.** `MCPP-<FAMILY><NN>`, the next free number in the family. IDs are stable:
31
+ never renumber or reuse one; a removed check goes into `RETIRED_IDS` in `registry.py`.
32
+ 3. **Write the check** in `src/mcp_posture/checks/<family>.py`:
33
+
34
+ ```python
35
+ @check(
36
+ id="MCPP-ASM14",
37
+ title="Short, specific title",
38
+ severity=Severity.MEDIUM,
39
+ confidence=Confidence.HIGH,
40
+ revisions=revisions_from(SpecRevision.R2025_11_25),
41
+ references=[R.MCP_AUTH_2025_11],
42
+ rationale="""Why it matters, for someone who has not read the spec.""",
43
+ remediation="""What to change, concretely.""",
44
+ )
45
+ def asm14(ctx: ScanContext) -> Iterator[Finding]:
46
+ ...
47
+ yield ctx.finding("MCPP-ASM14", "What was observed.", location=..., evidence=[...])
48
+ ```
49
+
50
+ Checks are pure functions of the frozen `ScanContext`: no network I/O inside a check. If you
51
+ need more data, collect it in `engine.collect` / `discovery.py` / `client.py` through the
52
+ `Fetcher` (which enforces the SSRF guard, size caps and redirect policy).
53
+ Give findings a `location` and, when one check can fire several times on the same location,
54
+ a distinct `key`, so fingerprints stay unique and stable across runs.
55
+ 4. **Test both ways.** Add a positive and a negative test marked
56
+ `@pytest.mark.check("positive", "MCPP-ASM14")` and `@pytest.mark.check("negative", ...)`.
57
+ `tests/test_catalogue.py` fails if either is missing. Prefer a fixture profile knob
58
+ (`McpProfile` / `AsProfile`) over a hand-built context when the check reads HTTP data.
59
+ 5. **Docs** are generated from the registry (`docs/gen_checks.py`); the README family table is
60
+ checked by `tests/test_docs.py`.
61
+
62
+ Regenerate golden reports when output changes on purpose:
63
+ `UPDATE_GOLDEN=1 uv run pytest tests/test_report.py`, then review the diff.
64
+
65
+ ## Commits and pull requests
66
+
67
+ Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/)
68
+ (`feat:`, `fix:`, `docs:`, `ci:`, `deps:`...). release-please derives versions and the changelog
69
+ from them, so a user-visible change needs `feat:` or `fix:`. A new check is a `feat:`; a
70
+ change of a check's default severity is a `feat:` with a note in the body.
71
+
72
+ Keep pull requests focused, with tests, and green CI.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Baptiste PIRAULT
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,229 @@
1
+ Metadata-Version: 2.5
2
+ Name: mcp-posture
3
+ Version: 1.0.0-rc1
4
+ Summary: Security posture scanner for remote MCP servers: OAuth, transport and tool surface, with spec-cited findings and SARIF.
5
+ Project-URL: Homepage, https://github.com/batou9150/mcp-posture
6
+ Project-URL: Documentation, https://batou9150.github.io/mcp-posture/
7
+ Project-URL: Issues, https://github.com/batou9150/mcp-posture/issues
8
+ Author: Baptiste PIRAULT
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: mcp,model-context-protocol,oauth,sarif,scanner,security
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Information Technology
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Security
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.12
21
+ Requires-Dist: httpx>=0.28
22
+ Requires-Dist: mcp-types<3,>=2.3
23
+ Requires-Dist: pydantic>=2.12
24
+ Requires-Dist: rich>=13.9
25
+ Requires-Dist: typer>=0.16
26
+ Description-Content-Type: text/markdown
27
+
28
+ # mcp-posture
29
+
30
+ [![CI](https://github.com/batou9150/mcp-posture/actions/workflows/ci.yml/badge.svg)](https://github.com/batou9150/mcp-posture/actions/workflows/ci.yml)
31
+ [![Action self-test](https://github.com/batou9150/mcp-posture/actions/workflows/action-selftest.yml/badge.svg)](https://github.com/batou9150/mcp-posture/actions/workflows/action-selftest.yml)
32
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
33
+
34
+ **Security posture scanner for remote MCP servers.** Point it at a Streamable HTTP endpoint and
35
+ it audits the OAuth setup (MCP Authorization spec, RFC 9728, RFC 8414, PKCE, RFC 8707, Client ID
36
+ Metadata Documents), transport hardening and the tool surface (poisoning, shadowing, rug pulls),
37
+ with spec-cited findings and CI-native output (SARIF, JSON, Markdown).
38
+
39
+ > Status: release candidate (`1.0.0-rc1`), not yet on PyPI. Only scan servers you own or are authorized to test.
40
+
41
+ ## Why
42
+
43
+ Most MCP scanners inspect local client configs and stdio servers. `mcp-posture` focuses on
44
+ **remote** servers and what an attacker sees from the outside before logging in: how the server
45
+ challenges, which authorization server it trusts, whether that server enforces PKCE S256, whether
46
+ tokens are audience-bound, whether the tool descriptions hide instructions. Every finding cites the
47
+ spec section and the MCP revision it applies to (`2025-03-26` through `2026-07-28`).
48
+
49
+ ## Quickstart
50
+
51
+ ```bash
52
+ # from source until the first PyPI release
53
+ uvx --from git+https://github.com/batou9150/mcp-posture mcp-posture scan https://mcp.example.com/mcp
54
+
55
+ # machine-readable outputs, fail the build on high or worse
56
+ mcp-posture scan https://mcp.example.com/mcp --sarif results.sarif --markdown summary.md --fail-on high
57
+
58
+ # lint your own client's Client ID Metadata Document
59
+ mcp-posture cimd lint https://app.example.com/oauth/client.json
60
+
61
+ # scan every remote server declared in your MCP clients (.mcp.json, Claude, VS Code, Cursor, Windsurf)
62
+ mcp-posture discover
63
+ mcp-posture scan --from-client-config auto
64
+
65
+ # list tools behind authentication: pass a token through the environment, never on the command line
66
+ MCP_TOKEN=... mcp-posture scan https://mcp.example.com/mcp --token-env MCP_TOKEN
67
+ ```
68
+
69
+ **Docker** (distroless, nonroot):
70
+
71
+ ```bash
72
+ docker build -t mcp-posture https://github.com/batou9150/mcp-posture.git
73
+ docker run --rm mcp-posture scan https://mcp.example.com/mcp
74
+ ```
75
+
76
+ **GitHub Action** (SARIF to code scanning, Markdown to the job summary):
77
+
78
+ ```yaml
79
+ - uses: batou9150/mcp-posture@main # pin a tag or SHA
80
+ with:
81
+ targets-file: mcp-servers.txt
82
+ fail-on: high
83
+ ```
84
+
85
+ **Claude Code skill**: the scanner plus a semantic review of tool descriptions, prioritization and
86
+ remediation snippets for common authorization servers and gateways.
87
+
88
+ ```text
89
+ /plugin marketplace add batou9150/mcp-posture
90
+ /plugin install mcp-posture@mcp-posture
91
+ ```
92
+
93
+ Then ask *"audit the security of https://mcp.example.com/mcp"*.
94
+
95
+ ![mcp-posture scanning local fixtures](docs/demo.gif)
96
+
97
+ Sample output (local misconfigured fixture, `scripts/demo_servers.py`):
98
+
99
+ ```
100
+ http://127.0.0.1:8402/mcp
101
+ spec 2025-06-18 (negotiated) · transport streamable-http · auth not required
102
+ critical MCPP-ASM04 code_challenge_methods_supported ['plain'] does not include S256.
103
+ high MCPP-ASM02 Metadata declares issuer 'http://127.0.0.1:8402/', expected 'http://127.0.0.1:8402'.
104
+ high MCPP-AUTHN01 tools/list answered without credentials (2 tool(s) including delete_note).
105
+ high MCPP-PRM06 bearer_methods_supported includes `query`.
106
+ high MCPP-TRN08 Session IDs look sequential (numerically close across two sessions).
107
+ medium MCPP-PRM03 resource '.../mcp/' does not match '.../mcp' (trailing slash only).
108
+ ...
109
+ ```
110
+
111
+ ## What it checks
112
+
113
+ | Family | Checks | Covers |
114
+ |---|---|---|
115
+ | `TRN` | 11 | HTTPS, TLS version and certificate, HTTP→HTTPS, HSTS, legacy SSE, session IDs in URLs, `Mcp-Session-Id` entropy, metadata headers, endpoint redirects |
116
+ | `AUTHN` | 6 | Anonymous `tools/list`, Bearer challenge, `resource_metadata` discovery, error leakage |
117
+ | `PRM` | 11 | RFC 9728 Protected Resource Metadata: presence, `resource` exact match, authorization servers, query tokens, scopes, signed metadata |
118
+ | `ASM` | 13 | RFC 8414 / OIDC metadata: issuer match, HTTPS endpoints, PKCE S256 / `plain`, implicit and password grants, DCR, `iss` (RFC 9207) |
119
+ | `CIMD` | 10 | Client ID Metadata Document support and registration strategy; `cimd lint` rules for your own client's document |
120
+ | `SCP` | 3 | Over-broad scopes, missing `scope` in challenges, PRM/AS scope consistency |
121
+ | `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 |
122
+ | `PIN` | 4 | Rug pulls: tools/prompts/resources added, removed or changed since `mcp-posture pin` |
123
+
124
+ `mcp-posture checks list` prints the catalogue; `mcp-posture checks show MCPP-ASM04` explains one
125
+ check (rationale, remediation, references). Check IDs are stable and never reused.
126
+ The research behind the catalogue, with every MUST/SHOULD mapped to a check, is in
127
+ [`docs/spec-notes.md`](docs/spec-notes.md).
128
+
129
+ The default mode is **passive**: metadata `GET`s plus the standard MCP handshake and list calls.
130
+ No tool is ever called. Behaviours that only an active probe could confirm (Origin validation,
131
+ token audience enforcement, PKCE enforcement) are listed as not verified in
132
+ [`docs/spec-notes.md`](docs/spec-notes.md).
133
+
134
+ ## How it works
135
+
136
+ ```mermaid
137
+ flowchart LR
138
+ T[Targets<br/>URLs · targets file · client configs] --> C[Collect<br/>hardened HTTP client]
139
+ C --> P[MCP prober<br/>server/discover → initialize → SSE]
140
+ C --> D[OAuth discovery<br/>401 challenge · PRM · AS metadata · TLS]
141
+ P & D --> X[Immutable scan context]
142
+ X --> K[Checks<br/>pure functions, registry]
143
+ K --> S[Suppressions · baseline]
144
+ S --> R[Reports<br/>table · JSON · SARIF · Markdown]
145
+ ```
146
+
147
+ Each target is collected once (all network I/O, through a client that refuses private addresses
148
+ at connect time), frozen into a context, then every check runs as a pure function over it. A
149
+ failing check becomes an `MCPP-ERR00` finding instead of aborting the scan.
150
+
151
+ ## CI usage
152
+
153
+ ```toml
154
+ # mcp-posture.toml (CLI flags override it)
155
+ [scan]
156
+ targets_file = "mcp-servers.txt"
157
+ fail_on = "high"
158
+ baseline = "mcp-posture.lock.json" # rug-pull detection, created with `mcp-posture pin`
159
+ disable = ["ASM08"]
160
+ ```
161
+
162
+ ```toml
163
+ # .mcp-posture-ignore: every suppression needs a justification; expired ones resurface
164
+ [[ignore]]
165
+ check = "MCPP-ASM09"
166
+ target = "https://mcp.example.com/*"
167
+ justification = "Open DCR is intended: public client registry"
168
+ expires = 2026-12-31
169
+ ```
170
+
171
+ SARIF results are anchored to the line of the file that declares each target (targets file,
172
+ `mcp-posture.toml`, `.mcp.json`), so GitHub code scanning can display them.
173
+
174
+ | Exit code | Meaning |
175
+ |---|---|
176
+ | `0` | No finding at or above `--fail-on`, every target reachable |
177
+ | `1` | At least one unsuppressed finding at or above `--fail-on` |
178
+ | `2` | Usage or configuration error |
179
+ | `3` | A target was unreachable (and no blocking finding) |
180
+
181
+ ## Safety of the scanner
182
+
183
+ - Tokens are read only from an env var, a file or stdin, and are redacted from every output and log.
184
+ - Private, loopback, link-local and cloud-metadata addresses are refused at connect time (after DNS
185
+ resolution, every redirect hop included) unless `--allow-private` is passed.
186
+ - Credentials are never forwarded across origins; response size and time are capped.
187
+ - Server-controlled text is neutralized in reports: no raw control, bidi or invisible characters
188
+ reach your terminal or PR comments.
189
+ - No telemetry.
190
+
191
+ ## Compared with other MCP scanners
192
+
193
+ Several good tools exist; they solve different problems. Facts as of 2026-10 (see
194
+ [`docs/spec-notes.md`](docs/spec-notes.md) for the survey):
195
+
196
+ | | mcp-posture | [Snyk agent-scan](https://github.com/snyk/agent-scan) | [Cisco mcp-scanner](https://github.com/cisco-ai-defense/mcp-scanner) | [Ramparts](https://github.com/highflame-ai/ramparts) | [MCPJam OAuth conformance](https://docs.mcpjam.com/cli/oauth-conformance) |
197
+ |---|---|---|---|---|---|
198
+ | Focus | remote server posture | local agent configs and tools | configs, stdio and remote servers | servers, configs, skills | live OAuth flow conformance |
199
+ | OAuth / PRM / AS metadata audit (pre-login) | yes, per RFC, per MCP revision | no | no | no | yes (needs a login) |
200
+ | Tool poisoning analysis | regex heuristics (+ semantic review via the skill) | remote API analysis | YARA + LLM | YARA + LLM | no |
201
+ | Rug-pull pinning | yes | signatures | no | yes | no |
202
+ | SARIF | yes | no | no | yes | no (JUnit) |
203
+
204
+ Pair `mcp-posture` with a runtime proxy (e.g. [mcp-context-protector](https://github.com/trailofbits/mcp-context-protector))
205
+ for enforcement; it reports, it does not block.
206
+
207
+ ## Responsible use
208
+
209
+ Scan only servers you own or have written permission to test. Passive mode sends a handful of
210
+ standard requests; even so, unsolicited scanning of third-party infrastructure may violate their
211
+ terms or the law.
212
+
213
+ ## Contributing and security
214
+
215
+ See [CONTRIBUTING.md](CONTRIBUTING.md) (how to add a check) and [SECURITY.md](SECURITY.md)
216
+ (reporting vulnerabilities, verifying release signatures and attestations).
217
+
218
+ ## Development
219
+
220
+ ```bash
221
+ uv sync
222
+ uv run ruff check && uv run ruff format --check && uv run mypy && uv run pytest
223
+ uv run python scripts/demo_servers.py # local fixtures to scan with --allow-private
224
+ uv run --group docs mkdocs serve # docs site
225
+ ```
226
+
227
+ ## License
228
+
229
+ [MIT](LICENSE) © Baptiste PIRAULT
@@ -0,0 +1,202 @@
1
+ # mcp-posture
2
+
3
+ [![CI](https://github.com/batou9150/mcp-posture/actions/workflows/ci.yml/badge.svg)](https://github.com/batou9150/mcp-posture/actions/workflows/ci.yml)
4
+ [![Action self-test](https://github.com/batou9150/mcp-posture/actions/workflows/action-selftest.yml/badge.svg)](https://github.com/batou9150/mcp-posture/actions/workflows/action-selftest.yml)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+
7
+ **Security posture scanner for remote MCP servers.** Point it at a Streamable HTTP endpoint and
8
+ it audits the OAuth setup (MCP Authorization spec, RFC 9728, RFC 8414, PKCE, RFC 8707, Client ID
9
+ Metadata Documents), transport hardening and the tool surface (poisoning, shadowing, rug pulls),
10
+ with spec-cited findings and CI-native output (SARIF, JSON, Markdown).
11
+
12
+ > Status: release candidate (`1.0.0-rc1`), not yet on PyPI. Only scan servers you own or are authorized to test.
13
+
14
+ ## Why
15
+
16
+ Most MCP scanners inspect local client configs and stdio servers. `mcp-posture` focuses on
17
+ **remote** servers and what an attacker sees from the outside before logging in: how the server
18
+ challenges, which authorization server it trusts, whether that server enforces PKCE S256, whether
19
+ tokens are audience-bound, whether the tool descriptions hide instructions. Every finding cites the
20
+ spec section and the MCP revision it applies to (`2025-03-26` through `2026-07-28`).
21
+
22
+ ## Quickstart
23
+
24
+ ```bash
25
+ # from source until the first PyPI release
26
+ uvx --from git+https://github.com/batou9150/mcp-posture mcp-posture scan https://mcp.example.com/mcp
27
+
28
+ # machine-readable outputs, fail the build on high or worse
29
+ mcp-posture scan https://mcp.example.com/mcp --sarif results.sarif --markdown summary.md --fail-on high
30
+
31
+ # lint your own client's Client ID Metadata Document
32
+ mcp-posture cimd lint https://app.example.com/oauth/client.json
33
+
34
+ # scan every remote server declared in your MCP clients (.mcp.json, Claude, VS Code, Cursor, Windsurf)
35
+ mcp-posture discover
36
+ mcp-posture scan --from-client-config auto
37
+
38
+ # list tools behind authentication: pass a token through the environment, never on the command line
39
+ MCP_TOKEN=... mcp-posture scan https://mcp.example.com/mcp --token-env MCP_TOKEN
40
+ ```
41
+
42
+ **Docker** (distroless, nonroot):
43
+
44
+ ```bash
45
+ docker build -t mcp-posture https://github.com/batou9150/mcp-posture.git
46
+ docker run --rm mcp-posture scan https://mcp.example.com/mcp
47
+ ```
48
+
49
+ **GitHub Action** (SARIF to code scanning, Markdown to the job summary):
50
+
51
+ ```yaml
52
+ - uses: batou9150/mcp-posture@main # pin a tag or SHA
53
+ with:
54
+ targets-file: mcp-servers.txt
55
+ fail-on: high
56
+ ```
57
+
58
+ **Claude Code skill**: the scanner plus a semantic review of tool descriptions, prioritization and
59
+ remediation snippets for common authorization servers and gateways.
60
+
61
+ ```text
62
+ /plugin marketplace add batou9150/mcp-posture
63
+ /plugin install mcp-posture@mcp-posture
64
+ ```
65
+
66
+ Then ask *"audit the security of https://mcp.example.com/mcp"*.
67
+
68
+ ![mcp-posture scanning local fixtures](docs/demo.gif)
69
+
70
+ Sample output (local misconfigured fixture, `scripts/demo_servers.py`):
71
+
72
+ ```
73
+ http://127.0.0.1:8402/mcp
74
+ spec 2025-06-18 (negotiated) · transport streamable-http · auth not required
75
+ critical MCPP-ASM04 code_challenge_methods_supported ['plain'] does not include S256.
76
+ high MCPP-ASM02 Metadata declares issuer 'http://127.0.0.1:8402/', expected 'http://127.0.0.1:8402'.
77
+ high MCPP-AUTHN01 tools/list answered without credentials (2 tool(s) including delete_note).
78
+ high MCPP-PRM06 bearer_methods_supported includes `query`.
79
+ high MCPP-TRN08 Session IDs look sequential (numerically close across two sessions).
80
+ medium MCPP-PRM03 resource '.../mcp/' does not match '.../mcp' (trailing slash only).
81
+ ...
82
+ ```
83
+
84
+ ## What it checks
85
+
86
+ | Family | Checks | Covers |
87
+ |---|---|---|
88
+ | `TRN` | 11 | HTTPS, TLS version and certificate, HTTP→HTTPS, HSTS, legacy SSE, session IDs in URLs, `Mcp-Session-Id` entropy, metadata headers, endpoint redirects |
89
+ | `AUTHN` | 6 | Anonymous `tools/list`, Bearer challenge, `resource_metadata` discovery, error leakage |
90
+ | `PRM` | 11 | RFC 9728 Protected Resource Metadata: presence, `resource` exact match, authorization servers, query tokens, scopes, signed metadata |
91
+ | `ASM` | 13 | RFC 8414 / OIDC metadata: issuer match, HTTPS endpoints, PKCE S256 / `plain`, implicit and password grants, DCR, `iss` (RFC 9207) |
92
+ | `CIMD` | 10 | Client ID Metadata Document support and registration strategy; `cimd lint` rules for your own client's document |
93
+ | `SCP` | 3 | Over-broad scopes, missing `scope` in challenges, PRM/AS scope consistency |
94
+ | `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 |
95
+ | `PIN` | 4 | Rug pulls: tools/prompts/resources added, removed or changed since `mcp-posture pin` |
96
+
97
+ `mcp-posture checks list` prints the catalogue; `mcp-posture checks show MCPP-ASM04` explains one
98
+ check (rationale, remediation, references). Check IDs are stable and never reused.
99
+ The research behind the catalogue, with every MUST/SHOULD mapped to a check, is in
100
+ [`docs/spec-notes.md`](docs/spec-notes.md).
101
+
102
+ The default mode is **passive**: metadata `GET`s plus the standard MCP handshake and list calls.
103
+ No tool is ever called. Behaviours that only an active probe could confirm (Origin validation,
104
+ token audience enforcement, PKCE enforcement) are listed as not verified in
105
+ [`docs/spec-notes.md`](docs/spec-notes.md).
106
+
107
+ ## How it works
108
+
109
+ ```mermaid
110
+ flowchart LR
111
+ T[Targets<br/>URLs · targets file · client configs] --> C[Collect<br/>hardened HTTP client]
112
+ C --> P[MCP prober<br/>server/discover → initialize → SSE]
113
+ C --> D[OAuth discovery<br/>401 challenge · PRM · AS metadata · TLS]
114
+ P & D --> X[Immutable scan context]
115
+ X --> K[Checks<br/>pure functions, registry]
116
+ K --> S[Suppressions · baseline]
117
+ S --> R[Reports<br/>table · JSON · SARIF · Markdown]
118
+ ```
119
+
120
+ Each target is collected once (all network I/O, through a client that refuses private addresses
121
+ at connect time), frozen into a context, then every check runs as a pure function over it. A
122
+ failing check becomes an `MCPP-ERR00` finding instead of aborting the scan.
123
+
124
+ ## CI usage
125
+
126
+ ```toml
127
+ # mcp-posture.toml (CLI flags override it)
128
+ [scan]
129
+ targets_file = "mcp-servers.txt"
130
+ fail_on = "high"
131
+ baseline = "mcp-posture.lock.json" # rug-pull detection, created with `mcp-posture pin`
132
+ disable = ["ASM08"]
133
+ ```
134
+
135
+ ```toml
136
+ # .mcp-posture-ignore: every suppression needs a justification; expired ones resurface
137
+ [[ignore]]
138
+ check = "MCPP-ASM09"
139
+ target = "https://mcp.example.com/*"
140
+ justification = "Open DCR is intended: public client registry"
141
+ expires = 2026-12-31
142
+ ```
143
+
144
+ SARIF results are anchored to the line of the file that declares each target (targets file,
145
+ `mcp-posture.toml`, `.mcp.json`), so GitHub code scanning can display them.
146
+
147
+ | Exit code | Meaning |
148
+ |---|---|
149
+ | `0` | No finding at or above `--fail-on`, every target reachable |
150
+ | `1` | At least one unsuppressed finding at or above `--fail-on` |
151
+ | `2` | Usage or configuration error |
152
+ | `3` | A target was unreachable (and no blocking finding) |
153
+
154
+ ## Safety of the scanner
155
+
156
+ - Tokens are read only from an env var, a file or stdin, and are redacted from every output and log.
157
+ - Private, loopback, link-local and cloud-metadata addresses are refused at connect time (after DNS
158
+ resolution, every redirect hop included) unless `--allow-private` is passed.
159
+ - Credentials are never forwarded across origins; response size and time are capped.
160
+ - Server-controlled text is neutralized in reports: no raw control, bidi or invisible characters
161
+ reach your terminal or PR comments.
162
+ - No telemetry.
163
+
164
+ ## Compared with other MCP scanners
165
+
166
+ Several good tools exist; they solve different problems. Facts as of 2026-10 (see
167
+ [`docs/spec-notes.md`](docs/spec-notes.md) for the survey):
168
+
169
+ | | mcp-posture | [Snyk agent-scan](https://github.com/snyk/agent-scan) | [Cisco mcp-scanner](https://github.com/cisco-ai-defense/mcp-scanner) | [Ramparts](https://github.com/highflame-ai/ramparts) | [MCPJam OAuth conformance](https://docs.mcpjam.com/cli/oauth-conformance) |
170
+ |---|---|---|---|---|---|
171
+ | Focus | remote server posture | local agent configs and tools | configs, stdio and remote servers | servers, configs, skills | live OAuth flow conformance |
172
+ | OAuth / PRM / AS metadata audit (pre-login) | yes, per RFC, per MCP revision | no | no | no | yes (needs a login) |
173
+ | Tool poisoning analysis | regex heuristics (+ semantic review via the skill) | remote API analysis | YARA + LLM | YARA + LLM | no |
174
+ | Rug-pull pinning | yes | signatures | no | yes | no |
175
+ | SARIF | yes | no | no | yes | no (JUnit) |
176
+
177
+ Pair `mcp-posture` with a runtime proxy (e.g. [mcp-context-protector](https://github.com/trailofbits/mcp-context-protector))
178
+ for enforcement; it reports, it does not block.
179
+
180
+ ## Responsible use
181
+
182
+ Scan only servers you own or have written permission to test. Passive mode sends a handful of
183
+ standard requests; even so, unsolicited scanning of third-party infrastructure may violate their
184
+ terms or the law.
185
+
186
+ ## Contributing and security
187
+
188
+ See [CONTRIBUTING.md](CONTRIBUTING.md) (how to add a check) and [SECURITY.md](SECURITY.md)
189
+ (reporting vulnerabilities, verifying release signatures and attestations).
190
+
191
+ ## Development
192
+
193
+ ```bash
194
+ uv sync
195
+ uv run ruff check && uv run ruff format --check && uv run mypy && uv run pytest
196
+ uv run python scripts/demo_servers.py # local fixtures to scan with --allow-private
197
+ uv run --group docs mkdocs serve # docs site
198
+ ```
199
+
200
+ ## License
201
+
202
+ [MIT](LICENSE) © Baptiste PIRAULT