runtime-mcp-scanner 0.1.1__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.
- runtime_mcp_scanner-0.1.1/.gitignore +39 -0
- runtime_mcp_scanner-0.1.1/LICENSE +21 -0
- runtime_mcp_scanner-0.1.1/PKG-INFO +302 -0
- runtime_mcp_scanner-0.1.1/README.md +275 -0
- runtime_mcp_scanner-0.1.1/mcp_scan/__init__.py +3 -0
- runtime_mcp_scanner-0.1.1/mcp_scan/__main__.py +6 -0
- runtime_mcp_scanner-0.1.1/mcp_scan/cli.py +82 -0
- runtime_mcp_scanner-0.1.1/mcp_scan/config_scan.py +208 -0
- runtime_mcp_scanner-0.1.1/mcp_scan/python_scan.py +264 -0
- runtime_mcp_scanner-0.1.1/mcp_scan/report.py +165 -0
- runtime_mcp_scanner-0.1.1/mcp_scan/rules.py +86 -0
- runtime_mcp_scanner-0.1.1/mcp_scan/rules.yaml +178 -0
- runtime_mcp_scanner-0.1.1/mcp_scan/scanner.py +79 -0
- runtime_mcp_scanner-0.1.1/mcp_scan/ts_scan.py +138 -0
- runtime_mcp_scanner-0.1.1/pyproject.toml +59 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*$py.class
|
|
4
|
+
*.so
|
|
5
|
+
.Python
|
|
6
|
+
build/
|
|
7
|
+
develop-eggs/
|
|
8
|
+
dist/
|
|
9
|
+
downloads/
|
|
10
|
+
eggs/
|
|
11
|
+
.eggs/
|
|
12
|
+
lib/
|
|
13
|
+
lib64/
|
|
14
|
+
parts/
|
|
15
|
+
sdist/
|
|
16
|
+
var/
|
|
17
|
+
wheels/
|
|
18
|
+
share/python-wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
MANIFEST
|
|
23
|
+
.pip-cache/
|
|
24
|
+
htmlcov/
|
|
25
|
+
.tox/
|
|
26
|
+
.nox/
|
|
27
|
+
.coverage
|
|
28
|
+
.coverage.*
|
|
29
|
+
.cache
|
|
30
|
+
.pytest_cache/
|
|
31
|
+
.env
|
|
32
|
+
.venv/
|
|
33
|
+
env/
|
|
34
|
+
venv/
|
|
35
|
+
ENV/
|
|
36
|
+
.idea/
|
|
37
|
+
.vscode/
|
|
38
|
+
*.swp
|
|
39
|
+
.DS_Store
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adam DiStefano
|
|
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,302 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: runtime-mcp-scanner
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Security scanner for MCP (Model Context Protocol) servers: scan configs and source code for shell execution, unscoped file access, missing auth, exfiltration paths, and shady package provenance.
|
|
5
|
+
Project-URL: Homepage, https://github.com/AdamDiStefanoAI/mcp-scanner
|
|
6
|
+
Project-URL: Repository, https://github.com/AdamDiStefanoAI/mcp-scanner
|
|
7
|
+
Project-URL: Issues, https://github.com/AdamDiStefanoAI/mcp-scanner/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/AdamDiStefanoAI/mcp-scanner/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Adam DiStefano
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agentic-ai,ai-security,llm,mcp,model-context-protocol,scanner,security,static-analysis
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Information Technology
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Security
|
|
23
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: pyyaml>=5.4
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# MCP-Scanner
|
|
29
|
+
|
|
30
|
+
[](https://pypi.org/project/runtime-mcp-scanner/)
|
|
31
|
+
[](https://pypi.org/project/runtime-mcp-scanner/)
|
|
32
|
+
[](https://opensource.org/licenses/MIT)
|
|
33
|
+
[](https://github.com/AdamDiStefanoAI/mcp-scanner/actions/workflows/ci.yml)
|
|
34
|
+
|
|
35
|
+
**Scan MCP server configs and source code for risky exposures before your agent connects to them.**
|
|
36
|
+
|
|
37
|
+
This week, researchers documented MONST3R-AI: a criminal toolkit built on stolen API keys and more than 800 fake AI-skill and MCP repositories. Malicious MCP servers are not a theory anymore. They are a distribution channel.
|
|
38
|
+
|
|
39
|
+
You would not install a random npm package without looking at it. Your agent connects to MCP servers with far less scrutiny, and an MCP server runs on your machine, with your credentials, able to offer your agent any tool it likes. A tool that runs shell commands. A tool that reads any file. A server that exfiltrates through tools that look boring on their own.
|
|
40
|
+
|
|
41
|
+
MCP-Scanner is a fast, dependency-light static check you run before you trust a server, and in CI after you do.
|
|
42
|
+
|
|
43
|
+
## Quickstart (30 seconds)
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# No install (uv)
|
|
47
|
+
uvx --from runtime-mcp-scanner mcp-scan path/to/config.json
|
|
48
|
+
|
|
49
|
+
# Or with pipx
|
|
50
|
+
pipx install runtime-mcp-scanner
|
|
51
|
+
mcp-scan path/to/server.py
|
|
52
|
+
|
|
53
|
+
# Or plain pip
|
|
54
|
+
pip install runtime-mcp-scanner
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Point it at client configs (Claude Desktop, Cursor, VS Code, Claude Code), Python servers (FastMCP and the low-level SDK), or TypeScript servers (`@modelcontextprotocol/sdk`). Directories are scanned recursively. The exit code is nonzero when any CRITICAL finding exists, so it drops straight into CI.
|
|
58
|
+
|
|
59
|
+
Try it on the intentionally vulnerable examples in this repo:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
git clone https://github.com/AdamDiStefanoAI/mcp-scanner
|
|
63
|
+
cd mcp-scanner
|
|
64
|
+
mcp-scan examples/vulnerable_server.py # 15 findings, exit 1
|
|
65
|
+
mcp-scan examples/vulnerable_config.json # 6 findings, exit 1
|
|
66
|
+
mcp-scan examples/safe_server.py # clean, exit 0
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Sample output
|
|
70
|
+
|
|
71
|
+
<details>
|
|
72
|
+
<summary>Full output of <code>mcp-scan examples/vulnerable_server.py</code></summary>
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
MCP-Scanner v0.1.0
|
|
76
|
+
|
|
77
|
+
CRITICAL (3)
|
|
78
|
+
[MCP-001] examples/vulnerable_server.py:20
|
|
79
|
+
Shell execution exposed
|
|
80
|
+
Evidence: subprocess.run(command, shell=True, capture_output=True, text=True)
|
|
81
|
+
Why it matters: A tool that runs shell commands gives anyone who can steer the agent, including
|
|
82
|
+
content injected into its context, arbitrary code execution on the host machine.
|
|
83
|
+
Fix: Remove shell execution from the tool surface. Replace it with a fixed allowlist of specific
|
|
84
|
+
operations that take validated, typed arguments.
|
|
85
|
+
|
|
86
|
+
[MCP-002] examples/vulnerable_server.py:26
|
|
87
|
+
Exfiltration path (network fetch plus file write)
|
|
88
|
+
Evidence: server combines network fetch with local file writes
|
|
89
|
+
Why it matters: One server that can both read local files or fetch network resources and write or
|
|
90
|
+
send data out is a complete exfiltration channel. Prompt injection only has to convince the agent
|
|
91
|
+
to use tools that already exist.
|
|
92
|
+
Fix: Split network access and file writes into separate servers with least privilege, and put an
|
|
93
|
+
egress allowlist in front of any fetch tool.
|
|
94
|
+
|
|
95
|
+
[MCP-001] examples/vulnerable_server.py:63
|
|
96
|
+
Shell execution exposed
|
|
97
|
+
Evidence: subprocess.run(f"pip install {package}", shell=True, capture_output=True)
|
|
98
|
+
Why it matters: A tool that runs shell commands gives anyone who can steer the agent, including
|
|
99
|
+
content injected into its context, arbitrary code execution on the host machine.
|
|
100
|
+
Fix: Remove shell execution from the tool surface. Replace it with a fixed allowlist of specific
|
|
101
|
+
operations that take validated, typed arguments.
|
|
102
|
+
|
|
103
|
+
HIGH (7)
|
|
104
|
+
[MCP-003] examples/vulnerable_server.py:27
|
|
105
|
+
Unscoped filesystem access
|
|
106
|
+
Evidence: open(dest, "w")
|
|
107
|
+
Why it matters: A file tool that takes a raw path from tool input can read or overwrite anything
|
|
108
|
+
the server process can reach: home directories, SSH keys, cloud credentials, other projects.
|
|
109
|
+
Fix: Pin file tools to an explicit root directory and reject any resolved path that escapes it
|
|
110
|
+
(resolve the path, then check the prefix).
|
|
111
|
+
|
|
112
|
+
[MCP-003] examples/vulnerable_server.py:34
|
|
113
|
+
Unscoped filesystem access
|
|
114
|
+
Evidence: open(file_path)
|
|
115
|
+
Why it matters: A file tool that takes a raw path from tool input can read or overwrite anything
|
|
116
|
+
the server process can reach: home directories, SSH keys, cloud credentials, other projects.
|
|
117
|
+
Fix: Pin file tools to an explicit root directory and reject any resolved path that escapes it
|
|
118
|
+
(resolve the path, then check the prefix).
|
|
119
|
+
|
|
120
|
+
[MCP-004] examples/vulnerable_server.py:40
|
|
121
|
+
Secret access
|
|
122
|
+
Evidence: os.environ[var_name]
|
|
123
|
+
Why it matters: Tools or configs that expose environment variables, credential files, or the cloud
|
|
124
|
+
metadata endpoint hand live secrets to the model context, where they can be logged, leaked, or
|
|
125
|
+
exfiltrated.
|
|
126
|
+
Fix: Never expose raw environment or credential stores as tool output. Scope credentials per
|
|
127
|
+
server, inject only what that server needs, and block the link-local metadata address at the
|
|
128
|
+
network layer.
|
|
129
|
+
|
|
130
|
+
[MCP-004] examples/vulnerable_server.py:45
|
|
131
|
+
Secret access
|
|
132
|
+
Evidence: ~/.ssh/id_rsa
|
|
133
|
+
Why it matters: Tools or configs that expose environment variables, credential files, or the cloud
|
|
134
|
+
metadata endpoint hand live secrets to the model context, where they can be logged, leaked, or
|
|
135
|
+
exfiltrated.
|
|
136
|
+
Fix: Never expose raw environment or credential stores as tool output. Scope credentials per
|
|
137
|
+
server, inject only what that server needs, and block the link-local metadata address at the
|
|
138
|
+
network layer.
|
|
139
|
+
|
|
140
|
+
[MCP-010] examples/vulnerable_server.py:58
|
|
141
|
+
Dynamic code execution
|
|
142
|
+
Evidence: eval(code)
|
|
143
|
+
Why it matters: eval, exec, and dynamic imports inside a tool handler turn any string the model
|
|
144
|
+
produces into running code, bypassing every other control in the server.
|
|
145
|
+
Fix: Remove dynamic evaluation from tool handlers. Replace it with a dispatch table of named,
|
|
146
|
+
audited operations.
|
|
147
|
+
|
|
148
|
+
[MCP-003] examples/vulnerable_server.py:70
|
|
149
|
+
Unscoped filesystem access
|
|
150
|
+
Evidence: open(config_path)
|
|
151
|
+
Why it matters: A file tool that takes a raw path from tool input can read or overwrite anything
|
|
152
|
+
the server process can reach: home directories, SSH keys, cloud credentials, other projects.
|
|
153
|
+
Fix: Pin file tools to an explicit root directory and reject any resolved path that escapes it
|
|
154
|
+
(resolve the path, then check the prefix).
|
|
155
|
+
|
|
156
|
+
[MCP-005] examples/vulnerable_server.py:75
|
|
157
|
+
Network transport without authentication
|
|
158
|
+
Evidence: network transport started with no authentication check in file
|
|
159
|
+
Why it matters: An MCP server listening on HTTP or SSE with no authentication lets anyone who can
|
|
160
|
+
reach the port call its tools directly, no agent required.
|
|
161
|
+
Fix: Require an Authorization header or token check on every request, bind to localhost when
|
|
162
|
+
remote access is not needed, and put the server behind a gateway that authenticates callers.
|
|
163
|
+
|
|
164
|
+
MEDIUM (5)
|
|
165
|
+
[MCP-007] examples/vulnerable_server.py:18
|
|
166
|
+
Overbroad tool description
|
|
167
|
+
Evidence: description of exec_shell: "anything"
|
|
168
|
+
Why it matters: Tool descriptions are instructions to the model. Descriptions that advertise
|
|
169
|
+
unrestricted capability ("run any command", "access all files") teach the agent to use the tool as
|
|
170
|
+
a general escape hatch and are a hallmark of malicious MCP packages.
|
|
171
|
+
Fix: Describe exactly what the tool does, what it may touch, and what it must never do. Treat
|
|
172
|
+
description text as part of your attack surface.
|
|
173
|
+
|
|
174
|
+
[MCP-007] examples/vulnerable_server.py:33
|
|
175
|
+
Overbroad tool description
|
|
176
|
+
Evidence: description of read_file: "any file"
|
|
177
|
+
Why it matters: Tool descriptions are instructions to the model. Descriptions that advertise
|
|
178
|
+
unrestricted capability ("run any command", "access all files") teach the agent to use the tool as
|
|
179
|
+
a general escape hatch and are a hallmark of malicious MCP packages.
|
|
180
|
+
Fix: Describe exactly what the tool does, what it may touch, and what it must never do. Treat
|
|
181
|
+
description text as part of your attack surface.
|
|
182
|
+
|
|
183
|
+
[MCP-006] examples/vulnerable_server.py:50
|
|
184
|
+
Mutating tool with no input schema
|
|
185
|
+
Evidence: tool delete_data mutates state but its parameters have no type/schema annotations
|
|
186
|
+
Why it matters: A write, delete, or execute tool with an empty input schema accepts arbitrary
|
|
187
|
+
arguments with no validation layer between the model and the action.
|
|
188
|
+
Fix: Give every mutating tool a strict input schema with typed, bounded fields, and validate
|
|
189
|
+
arguments server-side before acting on them.
|
|
190
|
+
|
|
191
|
+
[MCP-008] examples/vulnerable_server.py:63
|
|
192
|
+
Self-modifying server
|
|
193
|
+
Evidence: pip install
|
|
194
|
+
Why it matters: A tool that can rewrite MCP client configs or install new packages can persist
|
|
195
|
+
itself, add new malicious servers, and survive removal of the original package.
|
|
196
|
+
Fix: MCP servers must never write to client configuration files or invoke package managers. Keep
|
|
197
|
+
config changes manual and reviewable.
|
|
198
|
+
|
|
199
|
+
[MCP-008] examples/vulnerable_server.py:69
|
|
200
|
+
Self-modifying server
|
|
201
|
+
Evidence: /home/user/.config/claude/claude_desktop_config.json
|
|
202
|
+
Why it matters: A tool that can rewrite MCP client configs or install new packages can persist
|
|
203
|
+
itself, add new malicious servers, and survive removal of the original package.
|
|
204
|
+
Fix: MCP servers must never write to client configuration files or invoke package managers. Keep
|
|
205
|
+
config changes manual and reviewable.
|
|
206
|
+
|
|
207
|
+
Summary: 1 file(s) scanned, 1 server(s) found. Findings: 3 critical, 7 high, 5 medium, 0 low, 0 info.
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
</details>
|
|
211
|
+
|
|
212
|
+
## What it checks
|
|
213
|
+
|
|
214
|
+
| Rule | Severity | What it catches |
|
|
215
|
+
| --- | --- | --- |
|
|
216
|
+
| MCP-001 | CRITICAL | Shell execution exposed: `subprocess`, `os.system`, `child_process`, or a config whose `command` is a shell |
|
|
217
|
+
| MCP-002 | CRITICAL | Exfiltration path: one server that can both fetch the network and write files |
|
|
218
|
+
| MCP-003 | HIGH | Unscoped filesystem access: file tools driven by raw input paths with no root or allowlist check |
|
|
219
|
+
| MCP-004 | HIGH | Secret access: environment variables exposed as tools, credential file reads, the `169.254.169.254` metadata endpoint, plaintext secrets in config `env` |
|
|
220
|
+
| MCP-005 | HIGH | Network transport without authentication: HTTP/SSE servers or config `url` servers with no auth headers |
|
|
221
|
+
| MCP-006 | MEDIUM | Mutating tool with no input schema: write/delete/exec tools whose arguments are untyped or undeclared |
|
|
222
|
+
| MCP-007 | MEDIUM | Overbroad tool description: "run any command", "access all files", and other escape-hatch language aimed at the model |
|
|
223
|
+
| MCP-008 | MEDIUM | Self-modifying server: tools that rewrite MCP client configs or run `pip install` / `npm install` |
|
|
224
|
+
| MCP-009 | HIGH | Package provenance: unpinned or `latest` auto-installs via `npx -y` / `uvx`, and package names within edit distance 2 of well-known MCP packages (typosquats) |
|
|
225
|
+
| MCP-010 | HIGH | Dynamic code execution: `eval`, `exec`, `__import__`, `new Function` inside a tool handler |
|
|
226
|
+
|
|
227
|
+
Rules are data, not code: severities, explanations, fixes, and detection hints live in [`src/mcp_scan/rules.yaml`](src/mcp_scan/rules.yaml). Point `--rules` at your own copy to tune them.
|
|
228
|
+
|
|
229
|
+
## Output formats
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
mcp-scan server.py # grouped terminal report
|
|
233
|
+
mcp-scan server.py --format json # machine-readable findings
|
|
234
|
+
mcp-scan server.py --format sarif # SARIF 2.1.0 for GitHub code scanning
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## GitHub Action
|
|
238
|
+
|
|
239
|
+
Fail the build when a scanned server has a CRITICAL finding:
|
|
240
|
+
|
|
241
|
+
```yaml
|
|
242
|
+
name: mcp-security
|
|
243
|
+
on: [push, pull_request]
|
|
244
|
+
jobs:
|
|
245
|
+
scan:
|
|
246
|
+
runs-on: ubuntu-latest
|
|
247
|
+
steps:
|
|
248
|
+
- uses: actions/checkout@v4
|
|
249
|
+
- uses: AdamDiStefanoAI/MCP-Scanner@v0.1.1
|
|
250
|
+
with:
|
|
251
|
+
targets: "mcp/ examples/"
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
For SARIF upload to GitHub code scanning, run the CLI directly:
|
|
255
|
+
|
|
256
|
+
```yaml
|
|
257
|
+
- uses: actions/setup-python@v5
|
|
258
|
+
with:
|
|
259
|
+
python-version: "3.12"
|
|
260
|
+
- run: pip install runtime-mcp-scanner
|
|
261
|
+
- run: mcp-scan servers/ --format sarif > results.sarif
|
|
262
|
+
continue-on-error: true
|
|
263
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
264
|
+
with:
|
|
265
|
+
sarif_file: results.sarif
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## What a risky config looks like
|
|
269
|
+
|
|
270
|
+
```json
|
|
271
|
+
{
|
|
272
|
+
"mcpServers": {
|
|
273
|
+
"helper": {
|
|
274
|
+
"command": "npx",
|
|
275
|
+
"args": ["-y", "@modelcontextprotocol/server-filesytem"]
|
|
276
|
+
},
|
|
277
|
+
"shell": {
|
|
278
|
+
"command": "bash",
|
|
279
|
+
"args": ["-lc", "run the bridge"]
|
|
280
|
+
},
|
|
281
|
+
"docs": {
|
|
282
|
+
"url": "http://docs.internal:8000/sse"
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
That config gets flagged for a shell-wrapped server (MCP-001), an unpinned auto-install and a one-character typosquat of `server-filesystem` (MCP-009), and a remote server with no auth headers (MCP-005). The full version, with a plaintext database password for good measure, is in [`examples/vulnerable_config.json`](examples/vulnerable_config.json).
|
|
289
|
+
|
|
290
|
+
## Honest limits
|
|
291
|
+
|
|
292
|
+
MCP-Scanner is a heuristic static check, not a proof of safety. It flags visible risk patterns in code and config; it does not sandbox servers, trace data flow across files, or catch a server that behaves during review and turns later. Run it as one layer: before install, in CI, and whenever a config changes. A clean scan means "nothing obviously dangerous," which is a much better starting point than "nobody looked."
|
|
293
|
+
|
|
294
|
+
## Contributing
|
|
295
|
+
|
|
296
|
+
Issues and pull requests are welcome. New rules are the highest-value contribution: if a malicious or sloppy MCP pattern is circulating, open an issue with a sample. See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
297
|
+
|
|
298
|
+
## License
|
|
299
|
+
|
|
300
|
+
MIT. See [LICENSE](LICENSE).
|
|
301
|
+
|
|
302
|
+
Built by [Adam DiStefano](https://github.com/AdamDiStefanoAI), creator of the [ACR Standard](https://github.com/AdamDiStefanoAI/acr-standard) for runtime control of AI agents. Companion tool: [AI-Key-Scanner](https://github.com/AdamDiStefanoAI/AI-Key-Scanner).
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# MCP-Scanner
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/runtime-mcp-scanner/)
|
|
4
|
+
[](https://pypi.org/project/runtime-mcp-scanner/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://github.com/AdamDiStefanoAI/mcp-scanner/actions/workflows/ci.yml)
|
|
7
|
+
|
|
8
|
+
**Scan MCP server configs and source code for risky exposures before your agent connects to them.**
|
|
9
|
+
|
|
10
|
+
This week, researchers documented MONST3R-AI: a criminal toolkit built on stolen API keys and more than 800 fake AI-skill and MCP repositories. Malicious MCP servers are not a theory anymore. They are a distribution channel.
|
|
11
|
+
|
|
12
|
+
You would not install a random npm package without looking at it. Your agent connects to MCP servers with far less scrutiny, and an MCP server runs on your machine, with your credentials, able to offer your agent any tool it likes. A tool that runs shell commands. A tool that reads any file. A server that exfiltrates through tools that look boring on their own.
|
|
13
|
+
|
|
14
|
+
MCP-Scanner is a fast, dependency-light static check you run before you trust a server, and in CI after you do.
|
|
15
|
+
|
|
16
|
+
## Quickstart (30 seconds)
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# No install (uv)
|
|
20
|
+
uvx --from runtime-mcp-scanner mcp-scan path/to/config.json
|
|
21
|
+
|
|
22
|
+
# Or with pipx
|
|
23
|
+
pipx install runtime-mcp-scanner
|
|
24
|
+
mcp-scan path/to/server.py
|
|
25
|
+
|
|
26
|
+
# Or plain pip
|
|
27
|
+
pip install runtime-mcp-scanner
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Point it at client configs (Claude Desktop, Cursor, VS Code, Claude Code), Python servers (FastMCP and the low-level SDK), or TypeScript servers (`@modelcontextprotocol/sdk`). Directories are scanned recursively. The exit code is nonzero when any CRITICAL finding exists, so it drops straight into CI.
|
|
31
|
+
|
|
32
|
+
Try it on the intentionally vulnerable examples in this repo:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
git clone https://github.com/AdamDiStefanoAI/mcp-scanner
|
|
36
|
+
cd mcp-scanner
|
|
37
|
+
mcp-scan examples/vulnerable_server.py # 15 findings, exit 1
|
|
38
|
+
mcp-scan examples/vulnerable_config.json # 6 findings, exit 1
|
|
39
|
+
mcp-scan examples/safe_server.py # clean, exit 0
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Sample output
|
|
43
|
+
|
|
44
|
+
<details>
|
|
45
|
+
<summary>Full output of <code>mcp-scan examples/vulnerable_server.py</code></summary>
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
MCP-Scanner v0.1.0
|
|
49
|
+
|
|
50
|
+
CRITICAL (3)
|
|
51
|
+
[MCP-001] examples/vulnerable_server.py:20
|
|
52
|
+
Shell execution exposed
|
|
53
|
+
Evidence: subprocess.run(command, shell=True, capture_output=True, text=True)
|
|
54
|
+
Why it matters: A tool that runs shell commands gives anyone who can steer the agent, including
|
|
55
|
+
content injected into its context, arbitrary code execution on the host machine.
|
|
56
|
+
Fix: Remove shell execution from the tool surface. Replace it with a fixed allowlist of specific
|
|
57
|
+
operations that take validated, typed arguments.
|
|
58
|
+
|
|
59
|
+
[MCP-002] examples/vulnerable_server.py:26
|
|
60
|
+
Exfiltration path (network fetch plus file write)
|
|
61
|
+
Evidence: server combines network fetch with local file writes
|
|
62
|
+
Why it matters: One server that can both read local files or fetch network resources and write or
|
|
63
|
+
send data out is a complete exfiltration channel. Prompt injection only has to convince the agent
|
|
64
|
+
to use tools that already exist.
|
|
65
|
+
Fix: Split network access and file writes into separate servers with least privilege, and put an
|
|
66
|
+
egress allowlist in front of any fetch tool.
|
|
67
|
+
|
|
68
|
+
[MCP-001] examples/vulnerable_server.py:63
|
|
69
|
+
Shell execution exposed
|
|
70
|
+
Evidence: subprocess.run(f"pip install {package}", shell=True, capture_output=True)
|
|
71
|
+
Why it matters: A tool that runs shell commands gives anyone who can steer the agent, including
|
|
72
|
+
content injected into its context, arbitrary code execution on the host machine.
|
|
73
|
+
Fix: Remove shell execution from the tool surface. Replace it with a fixed allowlist of specific
|
|
74
|
+
operations that take validated, typed arguments.
|
|
75
|
+
|
|
76
|
+
HIGH (7)
|
|
77
|
+
[MCP-003] examples/vulnerable_server.py:27
|
|
78
|
+
Unscoped filesystem access
|
|
79
|
+
Evidence: open(dest, "w")
|
|
80
|
+
Why it matters: A file tool that takes a raw path from tool input can read or overwrite anything
|
|
81
|
+
the server process can reach: home directories, SSH keys, cloud credentials, other projects.
|
|
82
|
+
Fix: Pin file tools to an explicit root directory and reject any resolved path that escapes it
|
|
83
|
+
(resolve the path, then check the prefix).
|
|
84
|
+
|
|
85
|
+
[MCP-003] examples/vulnerable_server.py:34
|
|
86
|
+
Unscoped filesystem access
|
|
87
|
+
Evidence: open(file_path)
|
|
88
|
+
Why it matters: A file tool that takes a raw path from tool input can read or overwrite anything
|
|
89
|
+
the server process can reach: home directories, SSH keys, cloud credentials, other projects.
|
|
90
|
+
Fix: Pin file tools to an explicit root directory and reject any resolved path that escapes it
|
|
91
|
+
(resolve the path, then check the prefix).
|
|
92
|
+
|
|
93
|
+
[MCP-004] examples/vulnerable_server.py:40
|
|
94
|
+
Secret access
|
|
95
|
+
Evidence: os.environ[var_name]
|
|
96
|
+
Why it matters: Tools or configs that expose environment variables, credential files, or the cloud
|
|
97
|
+
metadata endpoint hand live secrets to the model context, where they can be logged, leaked, or
|
|
98
|
+
exfiltrated.
|
|
99
|
+
Fix: Never expose raw environment or credential stores as tool output. Scope credentials per
|
|
100
|
+
server, inject only what that server needs, and block the link-local metadata address at the
|
|
101
|
+
network layer.
|
|
102
|
+
|
|
103
|
+
[MCP-004] examples/vulnerable_server.py:45
|
|
104
|
+
Secret access
|
|
105
|
+
Evidence: ~/.ssh/id_rsa
|
|
106
|
+
Why it matters: Tools or configs that expose environment variables, credential files, or the cloud
|
|
107
|
+
metadata endpoint hand live secrets to the model context, where they can be logged, leaked, or
|
|
108
|
+
exfiltrated.
|
|
109
|
+
Fix: Never expose raw environment or credential stores as tool output. Scope credentials per
|
|
110
|
+
server, inject only what that server needs, and block the link-local metadata address at the
|
|
111
|
+
network layer.
|
|
112
|
+
|
|
113
|
+
[MCP-010] examples/vulnerable_server.py:58
|
|
114
|
+
Dynamic code execution
|
|
115
|
+
Evidence: eval(code)
|
|
116
|
+
Why it matters: eval, exec, and dynamic imports inside a tool handler turn any string the model
|
|
117
|
+
produces into running code, bypassing every other control in the server.
|
|
118
|
+
Fix: Remove dynamic evaluation from tool handlers. Replace it with a dispatch table of named,
|
|
119
|
+
audited operations.
|
|
120
|
+
|
|
121
|
+
[MCP-003] examples/vulnerable_server.py:70
|
|
122
|
+
Unscoped filesystem access
|
|
123
|
+
Evidence: open(config_path)
|
|
124
|
+
Why it matters: A file tool that takes a raw path from tool input can read or overwrite anything
|
|
125
|
+
the server process can reach: home directories, SSH keys, cloud credentials, other projects.
|
|
126
|
+
Fix: Pin file tools to an explicit root directory and reject any resolved path that escapes it
|
|
127
|
+
(resolve the path, then check the prefix).
|
|
128
|
+
|
|
129
|
+
[MCP-005] examples/vulnerable_server.py:75
|
|
130
|
+
Network transport without authentication
|
|
131
|
+
Evidence: network transport started with no authentication check in file
|
|
132
|
+
Why it matters: An MCP server listening on HTTP or SSE with no authentication lets anyone who can
|
|
133
|
+
reach the port call its tools directly, no agent required.
|
|
134
|
+
Fix: Require an Authorization header or token check on every request, bind to localhost when
|
|
135
|
+
remote access is not needed, and put the server behind a gateway that authenticates callers.
|
|
136
|
+
|
|
137
|
+
MEDIUM (5)
|
|
138
|
+
[MCP-007] examples/vulnerable_server.py:18
|
|
139
|
+
Overbroad tool description
|
|
140
|
+
Evidence: description of exec_shell: "anything"
|
|
141
|
+
Why it matters: Tool descriptions are instructions to the model. Descriptions that advertise
|
|
142
|
+
unrestricted capability ("run any command", "access all files") teach the agent to use the tool as
|
|
143
|
+
a general escape hatch and are a hallmark of malicious MCP packages.
|
|
144
|
+
Fix: Describe exactly what the tool does, what it may touch, and what it must never do. Treat
|
|
145
|
+
description text as part of your attack surface.
|
|
146
|
+
|
|
147
|
+
[MCP-007] examples/vulnerable_server.py:33
|
|
148
|
+
Overbroad tool description
|
|
149
|
+
Evidence: description of read_file: "any file"
|
|
150
|
+
Why it matters: Tool descriptions are instructions to the model. Descriptions that advertise
|
|
151
|
+
unrestricted capability ("run any command", "access all files") teach the agent to use the tool as
|
|
152
|
+
a general escape hatch and are a hallmark of malicious MCP packages.
|
|
153
|
+
Fix: Describe exactly what the tool does, what it may touch, and what it must never do. Treat
|
|
154
|
+
description text as part of your attack surface.
|
|
155
|
+
|
|
156
|
+
[MCP-006] examples/vulnerable_server.py:50
|
|
157
|
+
Mutating tool with no input schema
|
|
158
|
+
Evidence: tool delete_data mutates state but its parameters have no type/schema annotations
|
|
159
|
+
Why it matters: A write, delete, or execute tool with an empty input schema accepts arbitrary
|
|
160
|
+
arguments with no validation layer between the model and the action.
|
|
161
|
+
Fix: Give every mutating tool a strict input schema with typed, bounded fields, and validate
|
|
162
|
+
arguments server-side before acting on them.
|
|
163
|
+
|
|
164
|
+
[MCP-008] examples/vulnerable_server.py:63
|
|
165
|
+
Self-modifying server
|
|
166
|
+
Evidence: pip install
|
|
167
|
+
Why it matters: A tool that can rewrite MCP client configs or install new packages can persist
|
|
168
|
+
itself, add new malicious servers, and survive removal of the original package.
|
|
169
|
+
Fix: MCP servers must never write to client configuration files or invoke package managers. Keep
|
|
170
|
+
config changes manual and reviewable.
|
|
171
|
+
|
|
172
|
+
[MCP-008] examples/vulnerable_server.py:69
|
|
173
|
+
Self-modifying server
|
|
174
|
+
Evidence: /home/user/.config/claude/claude_desktop_config.json
|
|
175
|
+
Why it matters: A tool that can rewrite MCP client configs or install new packages can persist
|
|
176
|
+
itself, add new malicious servers, and survive removal of the original package.
|
|
177
|
+
Fix: MCP servers must never write to client configuration files or invoke package managers. Keep
|
|
178
|
+
config changes manual and reviewable.
|
|
179
|
+
|
|
180
|
+
Summary: 1 file(s) scanned, 1 server(s) found. Findings: 3 critical, 7 high, 5 medium, 0 low, 0 info.
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
</details>
|
|
184
|
+
|
|
185
|
+
## What it checks
|
|
186
|
+
|
|
187
|
+
| Rule | Severity | What it catches |
|
|
188
|
+
| --- | --- | --- |
|
|
189
|
+
| MCP-001 | CRITICAL | Shell execution exposed: `subprocess`, `os.system`, `child_process`, or a config whose `command` is a shell |
|
|
190
|
+
| MCP-002 | CRITICAL | Exfiltration path: one server that can both fetch the network and write files |
|
|
191
|
+
| MCP-003 | HIGH | Unscoped filesystem access: file tools driven by raw input paths with no root or allowlist check |
|
|
192
|
+
| MCP-004 | HIGH | Secret access: environment variables exposed as tools, credential file reads, the `169.254.169.254` metadata endpoint, plaintext secrets in config `env` |
|
|
193
|
+
| MCP-005 | HIGH | Network transport without authentication: HTTP/SSE servers or config `url` servers with no auth headers |
|
|
194
|
+
| MCP-006 | MEDIUM | Mutating tool with no input schema: write/delete/exec tools whose arguments are untyped or undeclared |
|
|
195
|
+
| MCP-007 | MEDIUM | Overbroad tool description: "run any command", "access all files", and other escape-hatch language aimed at the model |
|
|
196
|
+
| MCP-008 | MEDIUM | Self-modifying server: tools that rewrite MCP client configs or run `pip install` / `npm install` |
|
|
197
|
+
| MCP-009 | HIGH | Package provenance: unpinned or `latest` auto-installs via `npx -y` / `uvx`, and package names within edit distance 2 of well-known MCP packages (typosquats) |
|
|
198
|
+
| MCP-010 | HIGH | Dynamic code execution: `eval`, `exec`, `__import__`, `new Function` inside a tool handler |
|
|
199
|
+
|
|
200
|
+
Rules are data, not code: severities, explanations, fixes, and detection hints live in [`src/mcp_scan/rules.yaml`](src/mcp_scan/rules.yaml). Point `--rules` at your own copy to tune them.
|
|
201
|
+
|
|
202
|
+
## Output formats
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
mcp-scan server.py # grouped terminal report
|
|
206
|
+
mcp-scan server.py --format json # machine-readable findings
|
|
207
|
+
mcp-scan server.py --format sarif # SARIF 2.1.0 for GitHub code scanning
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## GitHub Action
|
|
211
|
+
|
|
212
|
+
Fail the build when a scanned server has a CRITICAL finding:
|
|
213
|
+
|
|
214
|
+
```yaml
|
|
215
|
+
name: mcp-security
|
|
216
|
+
on: [push, pull_request]
|
|
217
|
+
jobs:
|
|
218
|
+
scan:
|
|
219
|
+
runs-on: ubuntu-latest
|
|
220
|
+
steps:
|
|
221
|
+
- uses: actions/checkout@v4
|
|
222
|
+
- uses: AdamDiStefanoAI/MCP-Scanner@v0.1.1
|
|
223
|
+
with:
|
|
224
|
+
targets: "mcp/ examples/"
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
For SARIF upload to GitHub code scanning, run the CLI directly:
|
|
228
|
+
|
|
229
|
+
```yaml
|
|
230
|
+
- uses: actions/setup-python@v5
|
|
231
|
+
with:
|
|
232
|
+
python-version: "3.12"
|
|
233
|
+
- run: pip install runtime-mcp-scanner
|
|
234
|
+
- run: mcp-scan servers/ --format sarif > results.sarif
|
|
235
|
+
continue-on-error: true
|
|
236
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
237
|
+
with:
|
|
238
|
+
sarif_file: results.sarif
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## What a risky config looks like
|
|
242
|
+
|
|
243
|
+
```json
|
|
244
|
+
{
|
|
245
|
+
"mcpServers": {
|
|
246
|
+
"helper": {
|
|
247
|
+
"command": "npx",
|
|
248
|
+
"args": ["-y", "@modelcontextprotocol/server-filesytem"]
|
|
249
|
+
},
|
|
250
|
+
"shell": {
|
|
251
|
+
"command": "bash",
|
|
252
|
+
"args": ["-lc", "run the bridge"]
|
|
253
|
+
},
|
|
254
|
+
"docs": {
|
|
255
|
+
"url": "http://docs.internal:8000/sse"
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
That config gets flagged for a shell-wrapped server (MCP-001), an unpinned auto-install and a one-character typosquat of `server-filesystem` (MCP-009), and a remote server with no auth headers (MCP-005). The full version, with a plaintext database password for good measure, is in [`examples/vulnerable_config.json`](examples/vulnerable_config.json).
|
|
262
|
+
|
|
263
|
+
## Honest limits
|
|
264
|
+
|
|
265
|
+
MCP-Scanner is a heuristic static check, not a proof of safety. It flags visible risk patterns in code and config; it does not sandbox servers, trace data flow across files, or catch a server that behaves during review and turns later. Run it as one layer: before install, in CI, and whenever a config changes. A clean scan means "nothing obviously dangerous," which is a much better starting point than "nobody looked."
|
|
266
|
+
|
|
267
|
+
## Contributing
|
|
268
|
+
|
|
269
|
+
Issues and pull requests are welcome. New rules are the highest-value contribution: if a malicious or sloppy MCP pattern is circulating, open an issue with a sample. See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
270
|
+
|
|
271
|
+
## License
|
|
272
|
+
|
|
273
|
+
MIT. See [LICENSE](LICENSE).
|
|
274
|
+
|
|
275
|
+
Built by [Adam DiStefano](https://github.com/AdamDiStefanoAI), creator of the [ACR Standard](https://github.com/AdamDiStefanoAI/acr-standard) for runtime control of AI agents. Companion tool: [AI-Key-Scanner](https://github.com/AdamDiStefanoAI/AI-Key-Scanner).
|