mcp-runtime-audit 0.1.0__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_runtime_audit-0.1.0/LICENSE +21 -0
- mcp_runtime_audit-0.1.0/PKG-INFO +173 -0
- mcp_runtime_audit-0.1.0/README.md +157 -0
- mcp_runtime_audit-0.1.0/pyproject.toml +28 -0
- mcp_runtime_audit-0.1.0/setup.cfg +4 -0
- mcp_runtime_audit-0.1.0/src/mcp_audit/__init__.py +4 -0
- mcp_runtime_audit-0.1.0/src/mcp_audit/cli.py +173 -0
- mcp_runtime_audit-0.1.0/src/mcp_audit/cve_db.py +215 -0
- mcp_runtime_audit-0.1.0/src/mcp_audit/scanners.py +502 -0
- mcp_runtime_audit-0.1.0/src/mcp_runtime_audit.egg-info/PKG-INFO +173 -0
- mcp_runtime_audit-0.1.0/src/mcp_runtime_audit.egg-info/SOURCES.txt +15 -0
- mcp_runtime_audit-0.1.0/src/mcp_runtime_audit.egg-info/dependency_links.txt +1 -0
- mcp_runtime_audit-0.1.0/src/mcp_runtime_audit.egg-info/entry_points.txt +2 -0
- mcp_runtime_audit-0.1.0/src/mcp_runtime_audit.egg-info/top_level.txt +1 -0
- mcp_runtime_audit-0.1.0/tests/test_cli.py +152 -0
- mcp_runtime_audit-0.1.0/tests/test_cve_db.py +78 -0
- mcp_runtime_audit-0.1.0/tests/test_scanners.py +307 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 hao li
|
|
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,173 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcp-runtime-audit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local runtime security scanner for MCP servers — known-CVE checks, one-click fixes
|
|
5
|
+
Author-email: hao li <haoli5933@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/hahahahahahahahah6/mcp-audit
|
|
8
|
+
Keywords: mcp,security,scanner,cli,cve
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Topic :: Security
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# mcp-audit
|
|
18
|
+
|
|
19
|
+
A local runtime security scanner for MCP servers. Point it at your MCP client
|
|
20
|
+
configs and it flags the misconfigurations and known vulnerabilities that
|
|
21
|
+
actually get exploited — then offers one-click fixes. Zero third-party
|
|
22
|
+
dependencies, Python >= 3.9.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install mcp-runtime-audit
|
|
26
|
+
mcp-audit scan
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Why this exists
|
|
30
|
+
|
|
31
|
+
MCP servers run with your user's privileges and are driven by an AI agent on
|
|
32
|
+
your behalf — which makes them a very different threat surface from a library
|
|
33
|
+
you import. The evidence so far is not reassuring:
|
|
34
|
+
|
|
35
|
+
- **The official tooling itself has shipped RCE.** CVE-2025-49596 hit
|
|
36
|
+
Anthropic's MCP Inspector (CVSS 9.4): its local proxy accepted an
|
|
37
|
+
unauthenticated `/sse` request with `transportType=stdio&command=<cmd>` and
|
|
38
|
+
spawned it as a subprocess — visiting a malicious page was enough to run
|
|
39
|
+
code on the developer's machine. Fixed in Inspector 0.14.1.
|
|
40
|
+
- **Enterprise MCP apps are not immune.** CVE-2026-76404 (Splunk MCP Server
|
|
41
|
+
< 1.2.1, CVSS 9.1): unsafe deserialization in credential management let an
|
|
42
|
+
admin-role user execute arbitrary OS commands (Splunk advisory
|
|
43
|
+
SVD-2026-0808).
|
|
44
|
+
- **Anthropic treats stdio risk as "expected"** — the position is that the
|
|
45
|
+
security burden sits with whoever deploys the server, not the protocol.
|
|
46
|
+
That leaves every local config file as the real security boundary, and
|
|
47
|
+
those files are hand-edited JSON.
|
|
48
|
+
- **Unpatched servers are out there in production.** Five US federal
|
|
49
|
+
government MCP servers were found running unpatched, and internet-wide
|
|
50
|
+
scans keep turning up thousands of exposed MCP endpoints, the vast majority
|
|
51
|
+
without any authentication.
|
|
52
|
+
|
|
53
|
+
Nobody audits their own `~/.claude.json` by hand. mcp-audit does it in a
|
|
54
|
+
second, in CI-friendly form.
|
|
55
|
+
|
|
56
|
+
## How it differs
|
|
57
|
+
|
|
58
|
+
| Tool | Known-CVE version checks | One-click fixes | Focus |
|
|
59
|
+
|------|--------------------------|-----------------|-------|
|
|
60
|
+
| **mcp-audit** | ✅ bundled CVE table | ✅ `fix --apply` | runtime security |
|
|
61
|
+
| mcpscan | ❌ | ❌ | static config scanning |
|
|
62
|
+
| graygnatconsole mcp-audit-tool | ❌ | ❌ | config auditing |
|
|
63
|
+
| mcp-lint | n/a | n/a | **design quality** linting, not runtime security — no overlap |
|
|
64
|
+
|
|
65
|
+
mcpscan and graygnatconsole's mcp-audit-tool scan configs but neither maps
|
|
66
|
+
your installed server versions against known CVEs, and neither writes fixes
|
|
67
|
+
back. mcp-lint is a design-quality linter for MCP servers; mcp-audit is a
|
|
68
|
+
runtime security scanner for the servers you run — different layer, no
|
|
69
|
+
overlap.
|
|
70
|
+
|
|
71
|
+
## Quick start
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install mcp-runtime-audit
|
|
75
|
+
|
|
76
|
+
# scan default locations (~/.claude.json, ~/.codex/config.json,
|
|
77
|
+
# ~/.config/mcp/servers.json, ~/.mcp.json)
|
|
78
|
+
mcp-audit scan
|
|
79
|
+
|
|
80
|
+
# scan a specific file, JSON output for CI
|
|
81
|
+
mcp-audit scan --config ~/my-claude.json --format json
|
|
82
|
+
|
|
83
|
+
# confirm unauthenticated reachability with a live probe
|
|
84
|
+
# (one unauthenticated GET per endpoint)
|
|
85
|
+
mcp-audit scan --probe
|
|
86
|
+
|
|
87
|
+
# preview fixes (dry run), then apply them (backs up first)
|
|
88
|
+
mcp-audit fix --config ~/.claude.json
|
|
89
|
+
mcp-audit fix --config ~/.claude.json --apply
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Exit codes: `0` clean, `1` findings, `2` errors — CI-ready.
|
|
93
|
+
|
|
94
|
+
## What it checks
|
|
95
|
+
|
|
96
|
+
1. **Dangerous stdio commands (`STDIO-SHELL`, high).** A stdio entry wrapping
|
|
97
|
+
execution in `bash -c` / `sh -c` / `cmd /c` is command injection by
|
|
98
|
+
design — this is the Langflow CVE-2026-105697 pattern (CVSS 9.9). Flags
|
|
99
|
+
the server and shows the exact invocation.
|
|
100
|
+
2. **Unauthenticated SSE/HTTP endpoints (`UNAUTH-ENDPOINT`,
|
|
101
|
+
critical with `--probe`).** Endpoints with no `Authorization`/`X-API-Key`
|
|
102
|
+
header. `--probe` sends one unauthenticated GET to confirm the endpoint
|
|
103
|
+
actually answers — the CVE-2025-49596 shape.
|
|
104
|
+
3. **Bind addresses (`BIND-ADDR`, high).** Servers bound to `0.0.0.0` or `::`
|
|
105
|
+
in URLs, `--host` flags, or env vars are reachable beyond localhost.
|
|
106
|
+
4. **Known-CVE version checks (`KNOWN-CVE`, per-CVE severity).** Extracts
|
|
107
|
+
`package@version` from `npx`/`uvx` invocations and matches against the
|
|
108
|
+
bundled CVE table below.
|
|
109
|
+
|
|
110
|
+
Every finding carries a rule id, CVE id, severity, the evidence, and the fix
|
|
111
|
+
command — in both text and JSON output.
|
|
112
|
+
|
|
113
|
+
### One-click fix
|
|
114
|
+
|
|
115
|
+
`mcp-audit fix` defaults to a dry-run plan. With `--apply` it:
|
|
116
|
+
|
|
117
|
+
- rewrites `0.0.0.0` → `127.0.0.1` in URLs, `--host` flags, and env values,
|
|
118
|
+
- adds an `Authorization: Bearer ${MCP_TOKEN}` placeholder header to
|
|
119
|
+
unauthenticated endpoints (you fill in a real token),
|
|
120
|
+
- backs up the config to `<file>.bak-<timestamp>` before writing,
|
|
121
|
+
- links critical servers into agent-guard's `blocked_servers` list when an
|
|
122
|
+
agent-guard config is detected (see below).
|
|
123
|
+
|
|
124
|
+
### agent-guard integration
|
|
125
|
+
|
|
126
|
+
`mcp-audit scan --link-agent-guard` writes every critical server into
|
|
127
|
+
agent-guard's blocklist (config backed up first). If agent-guard isn't
|
|
128
|
+
installed, it prints the manual linking instruction instead.
|
|
129
|
+
|
|
130
|
+
## CVE table
|
|
131
|
+
|
|
132
|
+
Bundled advisory snapshot (2026-10). Re-check NVD / vendor releases before
|
|
133
|
+
using these for compliance reporting.
|
|
134
|
+
|
|
135
|
+
| CVE | Product | Affected | Fix | Class |
|
|
136
|
+
|-----|---------|----------|-----|-------|
|
|
137
|
+
| CVE-2025-49596 | @modelcontextprotocol/inspector | < 0.14.1 | 0.14.1 | Unauthenticated RCE, CVSS 9.4 |
|
|
138
|
+
| CVE-2026-76404 | splunk-mcp-server | < 1.2.1 | 1.2.1 | Unsafe deserialization RCE, CVSS 9.1 |
|
|
139
|
+
| CVE-2026-58201 | lokka (M365 MCP) | < 2.1.2 | 2.1.2 | SSRF leaking ARM bearer token |
|
|
140
|
+
| CVE-2026-105697 | langflow / langflow-base / lfx | < 1.10.3 | 1.10.3 | MCP stdio `bash -c` RCE, CVSS 9.9 |
|
|
141
|
+
| CVE-2026-89039 | (product mapping pending) | — | — | See NVD |
|
|
142
|
+
| CVE-2026-105793 | (product mapping pending) | — | — | See NVD |
|
|
143
|
+
| CVE-2026-94486 | (product mapping pending) | — | — | See NVD |
|
|
144
|
+
| CVE-2026-19807 | (product mapping pending) | — | — | See NVD |
|
|
145
|
+
|
|
146
|
+
## CI example
|
|
147
|
+
|
|
148
|
+
```yaml
|
|
149
|
+
# .github/workflows/mcp-audit.yml
|
|
150
|
+
- uses: actions/setup-python@v5
|
|
151
|
+
with: { python-version: "3.12" }
|
|
152
|
+
- run: pip install mcp-runtime-audit
|
|
153
|
+
- run: mcp-audit scan --config ./mcp-servers.json --format json
|
|
154
|
+
# exits 1 on findings -> fails the build
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Limitations
|
|
158
|
+
|
|
159
|
+
- Version detection reads `package@version` from your config's command/args
|
|
160
|
+
(npx/uvx/node style). Servers installed another way, or without a pinned
|
|
161
|
+
version, won't match the CVE table.
|
|
162
|
+
- `--probe` sends one unauthenticated GET per endpoint — no auth bypass
|
|
163
|
+
attempts, no payloads, and it never mutates anything. It still touches
|
|
164
|
+
the network; don't run it against hosts you don't own.
|
|
165
|
+
- The CVE table is a bundled snapshot, not a live feed. New CVEs need a new
|
|
166
|
+
release.
|
|
167
|
+
- Static checks can't see what a server does after it starts (dynamic tool
|
|
168
|
+
behavior, prompt injection in tool outputs). This is a config/runtime
|
|
169
|
+
surface scanner, not a runtime behavior monitor.
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
MIT.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# mcp-audit
|
|
2
|
+
|
|
3
|
+
A local runtime security scanner for MCP servers. Point it at your MCP client
|
|
4
|
+
configs and it flags the misconfigurations and known vulnerabilities that
|
|
5
|
+
actually get exploited — then offers one-click fixes. Zero third-party
|
|
6
|
+
dependencies, Python >= 3.9.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install mcp-runtime-audit
|
|
10
|
+
mcp-audit scan
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Why this exists
|
|
14
|
+
|
|
15
|
+
MCP servers run with your user's privileges and are driven by an AI agent on
|
|
16
|
+
your behalf — which makes them a very different threat surface from a library
|
|
17
|
+
you import. The evidence so far is not reassuring:
|
|
18
|
+
|
|
19
|
+
- **The official tooling itself has shipped RCE.** CVE-2025-49596 hit
|
|
20
|
+
Anthropic's MCP Inspector (CVSS 9.4): its local proxy accepted an
|
|
21
|
+
unauthenticated `/sse` request with `transportType=stdio&command=<cmd>` and
|
|
22
|
+
spawned it as a subprocess — visiting a malicious page was enough to run
|
|
23
|
+
code on the developer's machine. Fixed in Inspector 0.14.1.
|
|
24
|
+
- **Enterprise MCP apps are not immune.** CVE-2026-76404 (Splunk MCP Server
|
|
25
|
+
< 1.2.1, CVSS 9.1): unsafe deserialization in credential management let an
|
|
26
|
+
admin-role user execute arbitrary OS commands (Splunk advisory
|
|
27
|
+
SVD-2026-0808).
|
|
28
|
+
- **Anthropic treats stdio risk as "expected"** — the position is that the
|
|
29
|
+
security burden sits with whoever deploys the server, not the protocol.
|
|
30
|
+
That leaves every local config file as the real security boundary, and
|
|
31
|
+
those files are hand-edited JSON.
|
|
32
|
+
- **Unpatched servers are out there in production.** Five US federal
|
|
33
|
+
government MCP servers were found running unpatched, and internet-wide
|
|
34
|
+
scans keep turning up thousands of exposed MCP endpoints, the vast majority
|
|
35
|
+
without any authentication.
|
|
36
|
+
|
|
37
|
+
Nobody audits their own `~/.claude.json` by hand. mcp-audit does it in a
|
|
38
|
+
second, in CI-friendly form.
|
|
39
|
+
|
|
40
|
+
## How it differs
|
|
41
|
+
|
|
42
|
+
| Tool | Known-CVE version checks | One-click fixes | Focus |
|
|
43
|
+
|------|--------------------------|-----------------|-------|
|
|
44
|
+
| **mcp-audit** | ✅ bundled CVE table | ✅ `fix --apply` | runtime security |
|
|
45
|
+
| mcpscan | ❌ | ❌ | static config scanning |
|
|
46
|
+
| graygnatconsole mcp-audit-tool | ❌ | ❌ | config auditing |
|
|
47
|
+
| mcp-lint | n/a | n/a | **design quality** linting, not runtime security — no overlap |
|
|
48
|
+
|
|
49
|
+
mcpscan and graygnatconsole's mcp-audit-tool scan configs but neither maps
|
|
50
|
+
your installed server versions against known CVEs, and neither writes fixes
|
|
51
|
+
back. mcp-lint is a design-quality linter for MCP servers; mcp-audit is a
|
|
52
|
+
runtime security scanner for the servers you run — different layer, no
|
|
53
|
+
overlap.
|
|
54
|
+
|
|
55
|
+
## Quick start
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install mcp-runtime-audit
|
|
59
|
+
|
|
60
|
+
# scan default locations (~/.claude.json, ~/.codex/config.json,
|
|
61
|
+
# ~/.config/mcp/servers.json, ~/.mcp.json)
|
|
62
|
+
mcp-audit scan
|
|
63
|
+
|
|
64
|
+
# scan a specific file, JSON output for CI
|
|
65
|
+
mcp-audit scan --config ~/my-claude.json --format json
|
|
66
|
+
|
|
67
|
+
# confirm unauthenticated reachability with a live probe
|
|
68
|
+
# (one unauthenticated GET per endpoint)
|
|
69
|
+
mcp-audit scan --probe
|
|
70
|
+
|
|
71
|
+
# preview fixes (dry run), then apply them (backs up first)
|
|
72
|
+
mcp-audit fix --config ~/.claude.json
|
|
73
|
+
mcp-audit fix --config ~/.claude.json --apply
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Exit codes: `0` clean, `1` findings, `2` errors — CI-ready.
|
|
77
|
+
|
|
78
|
+
## What it checks
|
|
79
|
+
|
|
80
|
+
1. **Dangerous stdio commands (`STDIO-SHELL`, high).** A stdio entry wrapping
|
|
81
|
+
execution in `bash -c` / `sh -c` / `cmd /c` is command injection by
|
|
82
|
+
design — this is the Langflow CVE-2026-105697 pattern (CVSS 9.9). Flags
|
|
83
|
+
the server and shows the exact invocation.
|
|
84
|
+
2. **Unauthenticated SSE/HTTP endpoints (`UNAUTH-ENDPOINT`,
|
|
85
|
+
critical with `--probe`).** Endpoints with no `Authorization`/`X-API-Key`
|
|
86
|
+
header. `--probe` sends one unauthenticated GET to confirm the endpoint
|
|
87
|
+
actually answers — the CVE-2025-49596 shape.
|
|
88
|
+
3. **Bind addresses (`BIND-ADDR`, high).** Servers bound to `0.0.0.0` or `::`
|
|
89
|
+
in URLs, `--host` flags, or env vars are reachable beyond localhost.
|
|
90
|
+
4. **Known-CVE version checks (`KNOWN-CVE`, per-CVE severity).** Extracts
|
|
91
|
+
`package@version` from `npx`/`uvx` invocations and matches against the
|
|
92
|
+
bundled CVE table below.
|
|
93
|
+
|
|
94
|
+
Every finding carries a rule id, CVE id, severity, the evidence, and the fix
|
|
95
|
+
command — in both text and JSON output.
|
|
96
|
+
|
|
97
|
+
### One-click fix
|
|
98
|
+
|
|
99
|
+
`mcp-audit fix` defaults to a dry-run plan. With `--apply` it:
|
|
100
|
+
|
|
101
|
+
- rewrites `0.0.0.0` → `127.0.0.1` in URLs, `--host` flags, and env values,
|
|
102
|
+
- adds an `Authorization: Bearer ${MCP_TOKEN}` placeholder header to
|
|
103
|
+
unauthenticated endpoints (you fill in a real token),
|
|
104
|
+
- backs up the config to `<file>.bak-<timestamp>` before writing,
|
|
105
|
+
- links critical servers into agent-guard's `blocked_servers` list when an
|
|
106
|
+
agent-guard config is detected (see below).
|
|
107
|
+
|
|
108
|
+
### agent-guard integration
|
|
109
|
+
|
|
110
|
+
`mcp-audit scan --link-agent-guard` writes every critical server into
|
|
111
|
+
agent-guard's blocklist (config backed up first). If agent-guard isn't
|
|
112
|
+
installed, it prints the manual linking instruction instead.
|
|
113
|
+
|
|
114
|
+
## CVE table
|
|
115
|
+
|
|
116
|
+
Bundled advisory snapshot (2026-10). Re-check NVD / vendor releases before
|
|
117
|
+
using these for compliance reporting.
|
|
118
|
+
|
|
119
|
+
| CVE | Product | Affected | Fix | Class |
|
|
120
|
+
|-----|---------|----------|-----|-------|
|
|
121
|
+
| CVE-2025-49596 | @modelcontextprotocol/inspector | < 0.14.1 | 0.14.1 | Unauthenticated RCE, CVSS 9.4 |
|
|
122
|
+
| CVE-2026-76404 | splunk-mcp-server | < 1.2.1 | 1.2.1 | Unsafe deserialization RCE, CVSS 9.1 |
|
|
123
|
+
| CVE-2026-58201 | lokka (M365 MCP) | < 2.1.2 | 2.1.2 | SSRF leaking ARM bearer token |
|
|
124
|
+
| CVE-2026-105697 | langflow / langflow-base / lfx | < 1.10.3 | 1.10.3 | MCP stdio `bash -c` RCE, CVSS 9.9 |
|
|
125
|
+
| CVE-2026-89039 | (product mapping pending) | — | — | See NVD |
|
|
126
|
+
| CVE-2026-105793 | (product mapping pending) | — | — | See NVD |
|
|
127
|
+
| CVE-2026-94486 | (product mapping pending) | — | — | See NVD |
|
|
128
|
+
| CVE-2026-19807 | (product mapping pending) | — | — | See NVD |
|
|
129
|
+
|
|
130
|
+
## CI example
|
|
131
|
+
|
|
132
|
+
```yaml
|
|
133
|
+
# .github/workflows/mcp-audit.yml
|
|
134
|
+
- uses: actions/setup-python@v5
|
|
135
|
+
with: { python-version: "3.12" }
|
|
136
|
+
- run: pip install mcp-runtime-audit
|
|
137
|
+
- run: mcp-audit scan --config ./mcp-servers.json --format json
|
|
138
|
+
# exits 1 on findings -> fails the build
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Limitations
|
|
142
|
+
|
|
143
|
+
- Version detection reads `package@version` from your config's command/args
|
|
144
|
+
(npx/uvx/node style). Servers installed another way, or without a pinned
|
|
145
|
+
version, won't match the CVE table.
|
|
146
|
+
- `--probe` sends one unauthenticated GET per endpoint — no auth bypass
|
|
147
|
+
attempts, no payloads, and it never mutates anything. It still touches
|
|
148
|
+
the network; don't run it against hosts you don't own.
|
|
149
|
+
- The CVE table is a bundled snapshot, not a live feed. New CVEs need a new
|
|
150
|
+
release.
|
|
151
|
+
- Static checks can't see what a server does after it starts (dynamic tool
|
|
152
|
+
behavior, prompt injection in tool outputs). This is a config/runtime
|
|
153
|
+
surface scanner, not a runtime behavior monitor.
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mcp-runtime-audit"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Local runtime security scanner for MCP servers — known-CVE checks, one-click fixes"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "hao li", email = "haoli5933@gmail.com" }]
|
|
13
|
+
keywords = ["mcp", "security", "scanner", "cli", "cve"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Topic :: Security",
|
|
18
|
+
]
|
|
19
|
+
dependencies = []
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://github.com/hahahahahahahahah6/mcp-audit"
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
mcp-audit = "mcp_audit.cli:main"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.packages.find]
|
|
28
|
+
where = ["src"]
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""mcp-audit CLI: scan local MCP configs and optionally fix them."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from . import __version__
|
|
10
|
+
from .scanners import (
|
|
11
|
+
agent_guard_link,
|
|
12
|
+
apply_fixes,
|
|
13
|
+
discover_configs,
|
|
14
|
+
load_config,
|
|
15
|
+
plan_fixes,
|
|
16
|
+
run_all_scans,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
EXIT_OK = 0
|
|
20
|
+
EXIT_FINDINGS = 1
|
|
21
|
+
EXIT_ERROR = 2
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _parse_args(argv):
|
|
25
|
+
parser = argparse.ArgumentParser(
|
|
26
|
+
prog="mcp-audit",
|
|
27
|
+
description="Local runtime security scanner for MCP servers.",
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument("--version", action="version", version=__version__)
|
|
30
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
31
|
+
|
|
32
|
+
scan = sub.add_parser("scan", help="scan MCP configs for security issues")
|
|
33
|
+
scan.add_argument("--config", action="append", dest="configs",
|
|
34
|
+
metavar="PATH", default=[],
|
|
35
|
+
help="config file to scan (repeatable; defaults to "
|
|
36
|
+
"~/.claude.json and other known locations)")
|
|
37
|
+
scan.add_argument("--format", choices=["text", "json"], default="text",
|
|
38
|
+
help="output format")
|
|
39
|
+
scan.add_argument("--probe", action="store_true",
|
|
40
|
+
help="send one unauthenticated GET to each http/sse "
|
|
41
|
+
"endpoint to confirm reachability")
|
|
42
|
+
scan.add_argument("--timeout", type=float, default=4.0,
|
|
43
|
+
help="probe timeout in seconds (default 4.0)")
|
|
44
|
+
scan.add_argument("--link-agent-guard", action="store_true",
|
|
45
|
+
help="write critical servers into agent-guard's "
|
|
46
|
+
"blocklist when it is installed")
|
|
47
|
+
|
|
48
|
+
fix = sub.add_parser("fix", help="print or apply one-click fixes")
|
|
49
|
+
fix.add_argument("--config", action="append", dest="configs",
|
|
50
|
+
metavar="PATH", default=[],
|
|
51
|
+
help="config file to fix (repeatable)")
|
|
52
|
+
fix.add_argument("--apply", action="store_true",
|
|
53
|
+
help="write the fixes to the config file (backs up "
|
|
54
|
+
"first); without it only prints the plan")
|
|
55
|
+
return parser.parse_args(argv)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _load_all(paths):
|
|
59
|
+
servers, errors = [], []
|
|
60
|
+
for path in paths:
|
|
61
|
+
svcs, err = load_config(path)
|
|
62
|
+
if err:
|
|
63
|
+
errors.append(err)
|
|
64
|
+
servers.extend(svcs)
|
|
65
|
+
return servers, errors
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _format_text(findings, errors, n_servers, n_configs):
|
|
69
|
+
lines = []
|
|
70
|
+
if errors:
|
|
71
|
+
for e in errors:
|
|
72
|
+
lines.append("error: %s" % e)
|
|
73
|
+
if not findings:
|
|
74
|
+
lines.append("mcp-audit: %d server(s) in %d config(s): no findings"
|
|
75
|
+
% (n_servers, n_configs))
|
|
76
|
+
return "\n".join(lines)
|
|
77
|
+
sev_max = findings[0].severity
|
|
78
|
+
lines.append("mcp-audit: %d finding(s) across %d server(s) "
|
|
79
|
+
"(worst: %s):" % (len(findings), n_servers, sev_max))
|
|
80
|
+
for f in findings:
|
|
81
|
+
cve = " [%s]" % f.cve_id if f.cve_id else ""
|
|
82
|
+
lines.append("[%s]%s %s (%s): %s"
|
|
83
|
+
% (f.severity.upper(), cve, f.rule_id, f.server, f.evidence))
|
|
84
|
+
lines.append(" fix: %s" % f.fix)
|
|
85
|
+
return "\n".join(lines)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def cmd_scan(args):
|
|
89
|
+
paths = discover_configs(args.configs)
|
|
90
|
+
if not paths:
|
|
91
|
+
msg = {"findings": [], "errors": ["no config files found"],
|
|
92
|
+
"summary": {"servers": 0, "configs": 0}}
|
|
93
|
+
if args.format == "json":
|
|
94
|
+
print(json.dumps(msg, indent=2))
|
|
95
|
+
else:
|
|
96
|
+
print("mcp-audit: no MCP config files found "
|
|
97
|
+
"(checked ~/.claude.json, ~/.codex/config.json, "
|
|
98
|
+
"~/.config/mcp/servers.json, ~/.mcp.json); "
|
|
99
|
+
"pass --config PATH to scan a specific file")
|
|
100
|
+
return EXIT_ERROR
|
|
101
|
+
servers, errors = _load_all(paths)
|
|
102
|
+
findings = run_all_scans(servers, probe=args.probe, timeout=args.timeout)
|
|
103
|
+
if args.link_agent_guard and findings:
|
|
104
|
+
linked, msg = agent_guard_link(findings)
|
|
105
|
+
if args.format == "json":
|
|
106
|
+
pass # message folded into output below
|
|
107
|
+
else:
|
|
108
|
+
print(msg)
|
|
109
|
+
if args.format == "json":
|
|
110
|
+
print(json.dumps({
|
|
111
|
+
"findings": [f.to_dict() for f in findings],
|
|
112
|
+
"errors": errors,
|
|
113
|
+
"summary": {
|
|
114
|
+
"servers": len(servers),
|
|
115
|
+
"configs": len(paths),
|
|
116
|
+
"findings": len(findings),
|
|
117
|
+
"worst": findings[0].severity if findings else None,
|
|
118
|
+
},
|
|
119
|
+
}, indent=2))
|
|
120
|
+
else:
|
|
121
|
+
print(_format_text(findings, errors, len(servers), len(paths)))
|
|
122
|
+
if errors and not servers:
|
|
123
|
+
return EXIT_ERROR
|
|
124
|
+
return EXIT_FINDINGS if findings else EXIT_OK
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def cmd_fix(args):
|
|
128
|
+
paths = discover_configs(args.configs)
|
|
129
|
+
if not paths:
|
|
130
|
+
print("mcp-audit: no MCP config files found; pass --config PATH")
|
|
131
|
+
return EXIT_ERROR
|
|
132
|
+
rc = EXIT_OK
|
|
133
|
+
for path in paths:
|
|
134
|
+
servers, errors = _load_all([path])
|
|
135
|
+
findings = run_all_scans(servers)
|
|
136
|
+
plan, missing = plan_fixes(path, findings) if not errors else ([], [])
|
|
137
|
+
if args.apply:
|
|
138
|
+
if errors:
|
|
139
|
+
print("%s: error: %s" % (path, errors[0]))
|
|
140
|
+
rc = EXIT_ERROR
|
|
141
|
+
continue
|
|
142
|
+
if not plan:
|
|
143
|
+
print("%s: nothing fixable" % path)
|
|
144
|
+
continue
|
|
145
|
+
backup, applied = apply_fixes(path, findings)
|
|
146
|
+
print("%s: applied %d fix(es); backup at %s"
|
|
147
|
+
% (path, applied, backup))
|
|
148
|
+
else:
|
|
149
|
+
print("%s:" % path)
|
|
150
|
+
if not plan and not missing:
|
|
151
|
+
print(" nothing fixable")
|
|
152
|
+
for item in plan:
|
|
153
|
+
print(" - [%s] %s: %s" % (
|
|
154
|
+
item["rule_id"], item["server"], item["action"]))
|
|
155
|
+
for name in missing:
|
|
156
|
+
print(" - %s: server not found in config (skipped)" % name)
|
|
157
|
+
if plan:
|
|
158
|
+
print(" re-run with --apply to write these fixes "
|
|
159
|
+
"(config is backed up first)")
|
|
160
|
+
return rc
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def main(argv=None):
|
|
164
|
+
args = _parse_args(argv if argv is not None else sys.argv[1:])
|
|
165
|
+
if args.command == "scan":
|
|
166
|
+
return cmd_scan(args)
|
|
167
|
+
if args.command == "fix":
|
|
168
|
+
return cmd_fix(args)
|
|
169
|
+
return EXIT_ERROR
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
if __name__ == "__main__":
|
|
173
|
+
sys.exit(main())
|