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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ """mcp-audit: local runtime security scanner for MCP servers."""
2
+
3
+ __version__ = "0.1.0"
4
+ __all__ = ["__version__"]
@@ -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())