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.
- mcp_posture-1.0.0rc1/.gitignore +136 -0
- mcp_posture-1.0.0rc1/CHANGELOG.md +25 -0
- mcp_posture-1.0.0rc1/CONTRIBUTING.md +72 -0
- mcp_posture-1.0.0rc1/LICENSE +21 -0
- mcp_posture-1.0.0rc1/PKG-INFO +229 -0
- mcp_posture-1.0.0rc1/README.md +202 -0
- mcp_posture-1.0.0rc1/SECURITY.md +55 -0
- mcp_posture-1.0.0rc1/docs/schema/report-v1.json +406 -0
- mcp_posture-1.0.0rc1/pyproject.toml +111 -0
- mcp_posture-1.0.0rc1/scripts/__init__.py +0 -0
- mcp_posture-1.0.0rc1/scripts/check_version.py +61 -0
- mcp_posture-1.0.0rc1/scripts/demo_servers.py +103 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/__init__.py +3 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/__main__.py +5 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/checks/__init__.py +5 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/checks/_refs.py +85 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/checks/_util.py +60 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/checks/asm.py +391 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/checks/authn.py +263 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/checks/cimd.py +70 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/checks/pin.py +107 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/checks/prm.py +356 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/checks/scp.py +116 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/checks/tool.py +459 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/checks/trn.py +406 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/cimd_lint.py +449 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/cli.py +582 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/client.py +383 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/config.py +115 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/context.py +193 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/discovery.py +259 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/docs.py +99 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/engine.py +311 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/heuristics.py +305 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/models.py +212 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/net.py +597 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/pin.py +111 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/py.typed +0 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/redact.py +117 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/registry.py +98 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/report/__init__.py +64 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/report/json.py +24 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/report/markdown.py +101 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/report/sarif.py +172 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/report/table.py +74 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/sources.py +180 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/suppress.py +117 -0
- mcp_posture-1.0.0rc1/src/mcp_posture/surface.py +90 -0
- mcp_posture-1.0.0rc1/tests/__init__.py +0 -0
- mcp_posture-1.0.0rc1/tests/conftest.py +65 -0
- mcp_posture-1.0.0rc1/tests/fixtures/__init__.py +0 -0
- mcp_posture-1.0.0rc1/tests/fixtures/servers.py +449 -0
- mcp_posture-1.0.0rc1/tests/golden/report.json +181 -0
- mcp_posture-1.0.0rc1/tests/golden/report.md +27 -0
- mcp_posture-1.0.0rc1/tests/golden/report.sarif +2569 -0
- mcp_posture-1.0.0rc1/tests/golden/report.txt +26 -0
- mcp_posture-1.0.0rc1/tests/schemas/sarif-schema-2.1.0.json +3389 -0
- mcp_posture-1.0.0rc1/tests/test_action.py +115 -0
- mcp_posture-1.0.0rc1/tests/test_catalogue.py +68 -0
- mcp_posture-1.0.0rc1/tests/test_checks_asm.py +240 -0
- mcp_posture-1.0.0rc1/tests/test_checks_authn.py +106 -0
- mcp_posture-1.0.0rc1/tests/test_checks_cimd.py +39 -0
- mcp_posture-1.0.0rc1/tests/test_checks_pin.py +91 -0
- mcp_posture-1.0.0rc1/tests/test_checks_prm.py +185 -0
- mcp_posture-1.0.0rc1/tests/test_checks_scp.py +64 -0
- mcp_posture-1.0.0rc1/tests/test_checks_tool.py +233 -0
- mcp_posture-1.0.0rc1/tests/test_checks_trn.py +257 -0
- mcp_posture-1.0.0rc1/tests/test_cimd_lint.py +244 -0
- mcp_posture-1.0.0rc1/tests/test_cli.py +301 -0
- mcp_posture-1.0.0rc1/tests/test_client.py +147 -0
- mcp_posture-1.0.0rc1/tests/test_discovery.py +111 -0
- mcp_posture-1.0.0rc1/tests/test_docs.py +39 -0
- mcp_posture-1.0.0rc1/tests/test_engine.py +143 -0
- mcp_posture-1.0.0rc1/tests/test_fuzz.py +317 -0
- mcp_posture-1.0.0rc1/tests/test_heuristics.py +113 -0
- mcp_posture-1.0.0rc1/tests/test_net.py +278 -0
- mcp_posture-1.0.0rc1/tests/test_redact.py +98 -0
- mcp_posture-1.0.0rc1/tests/test_report.py +318 -0
- mcp_posture-1.0.0rc1/tests/test_suppress_config_sources.py +221 -0
- 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
|
+
[](https://github.com/batou9150/mcp-posture/actions/workflows/ci.yml)
|
|
31
|
+
[](https://github.com/batou9150/mcp-posture/actions/workflows/action-selftest.yml)
|
|
32
|
+
[](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
|
+

|
|
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
|
+
[](https://github.com/batou9150/mcp-posture/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/batou9150/mcp-posture/actions/workflows/action-selftest.yml)
|
|
5
|
+
[](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
|
+

|
|
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
|