mcp-skill-scanner 0.2.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.
Files changed (43) hide show
  1. mcp_skill_scanner-0.2.0/PKG-INFO +209 -0
  2. mcp_skill_scanner-0.2.0/README.md +195 -0
  3. mcp_skill_scanner-0.2.0/pyproject.toml +37 -0
  4. mcp_skill_scanner-0.2.0/pyproject.toml.orig +42 -0
  5. mcp_skill_scanner-0.2.0/src/mcpsec/__init__.py +3 -0
  6. mcp_skill_scanner-0.2.0/src/mcpsec/cli.py +300 -0
  7. mcp_skill_scanner-0.2.0/src/mcpsec/corpus.py +47 -0
  8. mcp_skill_scanner-0.2.0/src/mcpsec/discovery.py +90 -0
  9. mcp_skill_scanner-0.2.0/src/mcpsec/embedder.py +171 -0
  10. mcp_skill_scanner-0.2.0/src/mcpsec/engine.py +47 -0
  11. mcp_skill_scanner-0.2.0/src/mcpsec/errors.py +15 -0
  12. mcp_skill_scanner-0.2.0/src/mcpsec/model.py +184 -0
  13. mcp_skill_scanner-0.2.0/src/mcpsec/parsers/__init__.py +22 -0
  14. mcp_skill_scanner-0.2.0/src/mcpsec/parsers/_common.py +66 -0
  15. mcp_skill_scanner-0.2.0/src/mcpsec/parsers/_json_pos.py +151 -0
  16. mcp_skill_scanner-0.2.0/src/mcpsec/parsers/mcp_config.py +138 -0
  17. mcp_skill_scanner-0.2.0/src/mcpsec/parsers/package_json.py +69 -0
  18. mcp_skill_scanner-0.2.0/src/mcpsec/parsers/pyproject.py +85 -0
  19. mcp_skill_scanner-0.2.0/src/mcpsec/parsers/python_source.py +32 -0
  20. mcp_skill_scanner-0.2.0/src/mcpsec/parsers/requirements.py +50 -0
  21. mcp_skill_scanner-0.2.0/src/mcpsec/parsers/skill_markdown.py +82 -0
  22. mcp_skill_scanner-0.2.0/src/mcpsec/parsers/smithery_yaml.py +495 -0
  23. mcp_skill_scanner-0.2.0/src/mcpsec/py.typed +0 -0
  24. mcp_skill_scanner-0.2.0/src/mcpsec/rules/__init__.py +130 -0
  25. mcp_skill_scanner-0.2.0/src/mcpsec/rules/dependency.py +24 -0
  26. mcp_skill_scanner-0.2.0/src/mcpsec/rules/dependency_registry.py +296 -0
  27. mcp_skill_scanner-0.2.0/src/mcpsec/rules/mcp_permissions.py +204 -0
  28. mcp_skill_scanner-0.2.0/src/mcpsec/rules/parsing.py +39 -0
  29. mcp_skill_scanner-0.2.0/src/mcpsec/rules/remote.py +242 -0
  30. mcp_skill_scanner-0.2.0/src/mcpsec/rules/semantic.py +121 -0
  31. mcp_skill_scanner-0.2.0/src/mcpsec/rules/skill_patterns.py +153 -0
  32. mcp_skill_scanner-0.2.0/src/mcpsec/rules/taint.py +58 -0
  33. mcp_skill_scanner-0.2.0/src/mcpsec/sarif.py +123 -0
  34. mcp_skill_scanner-0.2.0/src/mcpsec/sources/__init__.py +28 -0
  35. mcp_skill_scanner-0.2.0/src/mcpsec/sources/_http.py +186 -0
  36. mcp_skill_scanner-0.2.0/src/mcpsec/sources/providers.py +235 -0
  37. mcp_skill_scanner-0.2.0/src/mcpsec/taint/__init__.py +7 -0
  38. mcp_skill_scanner-0.2.0/src/mcpsec/taint/analyzer.py +412 -0
  39. mcp_skill_scanner-0.2.0/src/mcpsec/text.py +26 -0
  40. mcp_skill_scanner-0.2.0/src/mcpsec/triage/__init__.py +6 -0
  41. mcp_skill_scanner-0.2.0/src/mcpsec/triage/graph.py +220 -0
  42. mcp_skill_scanner-0.2.0/src/mcpsec/triage/llm.py +61 -0
  43. mcp_skill_scanner-0.2.0/src/mcpsec/triage/tracing.py +101 -0
@@ -0,0 +1,209 @@
1
+ Metadata-Version: 2.3
2
+ Name: mcp-skill-scanner
3
+ Version: 0.2.0
4
+ Summary: Static security scanner for MCP servers and AI agent skills.
5
+ Requires-Dist: pyyaml>=6.0.3
6
+ Requires-Dist: langgraph>=1.2,<2 ; extra == 'agent'
7
+ Requires-Dist: langfuse>=5.1,<6 ; extra == 'agent'
8
+ Requires-Dist: onnxruntime==1.31.1 ; extra == 'semantic'
9
+ Requires-Dist: tokenizers==0.23.3 ; extra == 'semantic'
10
+ Requires-Python: >=3.13
11
+ Provides-Extra: agent
12
+ Provides-Extra: semantic
13
+ Description-Content-Type: text/markdown
14
+
15
+ # mcp-security-scanner
16
+
17
+ [![CI](https://github.com/A-Djihadi/mcp-security-scanner/actions/workflows/ci.yml/badge.svg)](https://github.com/A-Djihadi/mcp-security-scanner/actions/workflows/ci.yml)
18
+
19
+ Static security scanner for MCP servers and AI agent skills.
20
+
21
+ Detects excessive permissions (unrestricted shell, unsanitized SQL), risky
22
+ dependencies, and malicious patterns. Emits SARIF v2.1.0 for CI integration.
23
+
24
+ ## Install
25
+
26
+ Requires Python 3.13.
27
+
28
+ Install the published package from PyPI:
29
+
30
+ ```bash
31
+ pip install mcp-skill-scanner
32
+ ```
33
+
34
+ Opt into semantic detection with the `[semantic]` extra:
35
+
36
+ ```bash
37
+ pip install mcp-skill-scanner[semantic]
38
+ ```
39
+
40
+ Or install from source with [`uv`](https://docs.astral.sh/uv/):
41
+
42
+ ```bash
43
+ git clone https://github.com/A-Djihadi/mcp-security-scanner.git
44
+ cd mcp-security-scanner
45
+ uv sync
46
+ ```
47
+
48
+ ## Usage
49
+
50
+ ```bash
51
+ # Scan a directory, write SARIF to a file (default format)
52
+ uv run mcpsec scan . --output results.sarif
53
+
54
+ # Human-readable output
55
+ uv run mcpsec scan . --format text
56
+
57
+ # Only error-level findings
58
+ uv run mcpsec scan . --severity error
59
+
60
+ # Only specific rules
61
+ uv run mcpsec scan . --rules MCP-shell-unrestricted,DEP-unpinned
62
+
63
+ # Exclude a rule
64
+ uv run mcpsec scan . --exclude-rule SKL-shell-execution
65
+
66
+ # Skip paths (globs, repeatable), e.g. to scan a repo that contains test fixtures
67
+ uv run mcpsec scan . --exclude tests/fixtures
68
+
69
+ # Network-backed rules (OSV.dev, PyPI/npm, reachability) -- opt-in
70
+ uv run mcpsec scan . --online
71
+ ```
72
+
73
+ ### Options
74
+
75
+ | Flag | Description |
76
+ |------|-------------|
77
+ | `--format sarif\|text` | Output format (default `sarif`). |
78
+ | `--output FILE` | Write output to `FILE` instead of stdout. |
79
+ | `--severity error\|warning\|note` | Only report findings at/above this severity. |
80
+ | `--rules IDS` | Comma-separated rule IDs to run; all others are disabled. |
81
+ | `--exclude-rule IDS` | Comma-separated rule IDs to disable. |
82
+ | `--exclude GLOB` | Skip paths matching `GLOB` (relative to the scan root, matches parent directories too; repeatable). |
83
+ | `--online` | Enable network-backed rules (`DEP-known-vulnerability`, `DEP-outdated`, `DEP-unknown`, `MCP-unreachable`). Only `http(s)` is fetched; private, loopback and link-local addresses are never probed. |
84
+ | `--fail-on error\|warning\|note` | Minimum severity that fails the scan (default `error`). |
85
+ | `--semantic` | Enable semantic malicious-intent detection via the local embedding model (requires the `[semantic]` extra). |
86
+
87
+ ### Semantic detection (optional)
88
+
89
+ `--semantic` opts into embedding-based malicious-intent detection for skills
90
+ (`SKL-semantic-malicious`). It is **off by default**: the base install stays
91
+ PyYAML-only, and a default scan never loads the model runtime.
92
+
93
+ ```bash
94
+ # Opt into the heavier install once:
95
+ uv sync --extra semantic # or: pip install mcp-skill-scanner[semantic]
96
+
97
+ # Then scan with semantic detection enabled:
98
+ uv run mcpsec scan . --semantic
99
+ ```
100
+
101
+ The default embedder is a small, CPU-only ONNX sentence-transformer:
102
+
103
+ - **Model:** `all-MiniLM-L6-v2` (ONNX export `Xenova/all-MiniLM-L6-v2`), Apache-2.0.
104
+ - **Pinned revision:** `751bff37182d3f1213fa05d7196b954e230abad9` (files verified by SHA-256).
105
+ - **Corpus:** a curated, versioned set of malicious-intent anchors
106
+ (`src/mcpsec/corpus.py`, version `1`); the model revision and corpus version
107
+ are recorded on every `SKL-semantic-malicious` finding for reproducibility.
108
+
109
+ **To verify:** the similarity threshold (default `0.85`) and the corpus anchor
110
+ coverage are calibrated against synthetic probes, not a production corpus; they
111
+ should be re-tuned against real-world skill data before relying on this rule as
112
+ a CI gate.
113
+
114
+ ### Exit codes
115
+
116
+ | Code | Meaning |
117
+ |------|---------|
118
+ | 0 | Scan completed; no findings at or above `--fail-on`. |
119
+ | 1 | Scan completed; one or more findings at or above `--fail-on`. |
120
+ | 2 | Internal error (bad args, unreadable path, parse error). |
121
+
122
+ ## Rules
123
+
124
+ | Rule ID | Level | Detects |
125
+ |---------|-------|---------|
126
+ | `MCP-shell-unrestricted` | error | Server `command` is a shell interpreter (`sh`, `bash`, `zsh`, `dash`, `ksh`, `fish`, `csh`, `powershell`, `pwsh`, `cmd`…), including behind a launcher (`env bash`, `sudo sh`). |
127
+ | `MCP-allow-all` | error | `args` contains `--allow-all` / `--allow-everything` / `-A`. |
128
+ | `MCP-broad-network` | error | `--allow-net` with no host restriction (bare flag or empty value). |
129
+ | `MCP-inline-code` | warning | Interpreter runs code passed on the command line (`python -c`, `node -e`, `ruby -e`, `perl -e`, `php -r`). |
130
+ | `MCP-insecure-transport` | error | Remote MCP server uses plaintext `http://`. |
131
+ | `MCP-header-secret` | warning | A secret is stored inline in a remote server's `headers`. |
132
+ | `MCP-remote-no-auth` | note | Remote MCP server has no auth-bearing header. |
133
+ | `MCP-parse-ambiguous` | note | Server command cannot be statically resolved from the config. |
134
+ | `MCP-unreachable` | note | Remote endpoint refused the connection (`--online`). |
135
+ | `FLOW-shell-injection` | error | Tainted MCP tool input reaches a shell execution sink (Python taint analysis). |
136
+ | `FLOW-sql-injection` | error | Tainted input reaches a SQL query sink. |
137
+ | `FLOW-code-eval` | warning | Tainted input reaches `eval` / `exec` / `compile`. |
138
+ | `MCP-broad-filesystem` | warning | `--allow-read` / `--allow-write` at a root-level path (`/`, `~`, `C:\`, `C:/`) or with no path. |
139
+ | `SKL-shell-execution` | warning | Skill body invokes a shell with interpolated input (f-string / concatenation). |
140
+ | `SKL-remote-code-exec` | error | Skill body pipes remote content to a shell (`curl … \| sh`, `wget … \| bash`, `eval(fetch(…))`). |
141
+ | `SKL-obfuscated-command` | warning | Shell command decodes a `base64` payload (`base64 -d` / `--decode`). |
142
+ | `SKL-sql-concatenation` | error | SQL built by string concatenation/interpolation; parameterized `?` queries do not trigger. |
143
+ | `DEP-unpinned` | warning | A direct dependency whose version spec is unpinned (`*`, `latest`, `^`, `~`, empty). |
144
+ | `DEP-known-vulnerability` | error | Declared version has a known vulnerability in OSV.dev (`--online`). |
145
+ | `DEP-outdated` | warning | Declared version is behind the latest published one (`--online`). |
146
+ | `DEP-unknown` | warning | Name closely resembles a popular package, a typosquat signal (`--online`). |
147
+ | `SKL-semantic-malicious` | warning | Skill prose is semantically similar to a known-malicious reference (opt-in via `--semantic`). |
148
+
149
+ ## LLM triage (optional)
150
+
151
+ `mcpsec triage` runs the same scan, then explains each finding and proposes a fix with a
152
+ **local** model (Ollama). It is advisory: findings, levels and the exit code are never
153
+ changed by the model ([ADR-0011](docs/adr/0011-llm-triage-advisory.md)).
154
+
155
+ ```bash
156
+ uv sync --extra agent # LangGraph + Langfuse
157
+ ollama pull qwen2.5-coder:3b
158
+ uv run mcpsec triage . --exclude tests/fixtures # text report
159
+ uv run mcpsec triage . --format sarif --output triage.sarif # SARIF + properties.triage
160
+ ```
161
+
162
+ How it works: a LangGraph graph fans out one branch per finding (`Send`); each branch loads
163
+ the file excerpt, asks the model for `explanation`, `fix` and `evidence`, and keeps the answer
164
+ only if the evidence line really exists in the file (one stricter retry, then a rule-based
165
+ explanation). The excerpt is passed as untrusted data, so instructions hidden in a scanned
166
+ skill cannot change the result. Set `LANGFUSE_BASE_URL`, `LANGFUSE_PUBLIC_KEY` and
167
+ `LANGFUSE_SECRET_KEY` to trace every node, model call (tokens) and `grounded` score in a
168
+ self-hosted Langfuse.
169
+
170
+ ## CI integration
171
+
172
+ The scanner is deterministic and offline, so it fits a CI gate. This repository
173
+ ships its own GitHub Actions workflow —
174
+ [`.github/workflows/ci.yml`](.github/workflows/ci.yml) — which lints (ruff), runs the
175
+ test suite, **scans this repository with mcpsec itself** and uploads the SARIF to the
176
+ Security tab, and runs a gitleaks secrets scan on every push and pull request.
177
+
178
+ To gate another project on `mcpsec` (actions pinned by commit SHA, as in this repo):
179
+
180
+ ```yaml
181
+ jobs:
182
+ mcpsec:
183
+ runs-on: ubuntu-latest
184
+ permissions:
185
+ contents: read
186
+ security-events: write # required to upload SARIF
187
+ steps:
188
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
189
+ with:
190
+ persist-credentials: false
191
+ - uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
192
+ - name: Scan for risky MCP configs and skills
193
+ run: uvx --from git+https://github.com/A-Djihadi/mcp-security-scanner mcpsec scan . --output results.sarif
194
+ - name: Upload SARIF
195
+ if: always()
196
+ uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
197
+ with:
198
+ sarif_file: results.sarif
199
+ ```
200
+
201
+ `uvx --from git+…` installs mcpsec in an isolated environment (pin it with `@<tag or sha>`
202
+ in real use); `uv run mcpsec` only works inside this repository.
203
+
204
+ The default `--fail-on error` makes `mcpsec` exit non-zero when an error-level
205
+ finding exists, which blocks the build.
206
+
207
+ ## License
208
+
209
+ [MIT](LICENSE) © 2026 Djihadi Ahamdy.
@@ -0,0 +1,195 @@
1
+ # mcp-security-scanner
2
+
3
+ [![CI](https://github.com/A-Djihadi/mcp-security-scanner/actions/workflows/ci.yml/badge.svg)](https://github.com/A-Djihadi/mcp-security-scanner/actions/workflows/ci.yml)
4
+
5
+ Static security scanner for MCP servers and AI agent skills.
6
+
7
+ Detects excessive permissions (unrestricted shell, unsanitized SQL), risky
8
+ dependencies, and malicious patterns. Emits SARIF v2.1.0 for CI integration.
9
+
10
+ ## Install
11
+
12
+ Requires Python 3.13.
13
+
14
+ Install the published package from PyPI:
15
+
16
+ ```bash
17
+ pip install mcp-skill-scanner
18
+ ```
19
+
20
+ Opt into semantic detection with the `[semantic]` extra:
21
+
22
+ ```bash
23
+ pip install mcp-skill-scanner[semantic]
24
+ ```
25
+
26
+ Or install from source with [`uv`](https://docs.astral.sh/uv/):
27
+
28
+ ```bash
29
+ git clone https://github.com/A-Djihadi/mcp-security-scanner.git
30
+ cd mcp-security-scanner
31
+ uv sync
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ```bash
37
+ # Scan a directory, write SARIF to a file (default format)
38
+ uv run mcpsec scan . --output results.sarif
39
+
40
+ # Human-readable output
41
+ uv run mcpsec scan . --format text
42
+
43
+ # Only error-level findings
44
+ uv run mcpsec scan . --severity error
45
+
46
+ # Only specific rules
47
+ uv run mcpsec scan . --rules MCP-shell-unrestricted,DEP-unpinned
48
+
49
+ # Exclude a rule
50
+ uv run mcpsec scan . --exclude-rule SKL-shell-execution
51
+
52
+ # Skip paths (globs, repeatable), e.g. to scan a repo that contains test fixtures
53
+ uv run mcpsec scan . --exclude tests/fixtures
54
+
55
+ # Network-backed rules (OSV.dev, PyPI/npm, reachability) -- opt-in
56
+ uv run mcpsec scan . --online
57
+ ```
58
+
59
+ ### Options
60
+
61
+ | Flag | Description |
62
+ |------|-------------|
63
+ | `--format sarif\|text` | Output format (default `sarif`). |
64
+ | `--output FILE` | Write output to `FILE` instead of stdout. |
65
+ | `--severity error\|warning\|note` | Only report findings at/above this severity. |
66
+ | `--rules IDS` | Comma-separated rule IDs to run; all others are disabled. |
67
+ | `--exclude-rule IDS` | Comma-separated rule IDs to disable. |
68
+ | `--exclude GLOB` | Skip paths matching `GLOB` (relative to the scan root, matches parent directories too; repeatable). |
69
+ | `--online` | Enable network-backed rules (`DEP-known-vulnerability`, `DEP-outdated`, `DEP-unknown`, `MCP-unreachable`). Only `http(s)` is fetched; private, loopback and link-local addresses are never probed. |
70
+ | `--fail-on error\|warning\|note` | Minimum severity that fails the scan (default `error`). |
71
+ | `--semantic` | Enable semantic malicious-intent detection via the local embedding model (requires the `[semantic]` extra). |
72
+
73
+ ### Semantic detection (optional)
74
+
75
+ `--semantic` opts into embedding-based malicious-intent detection for skills
76
+ (`SKL-semantic-malicious`). It is **off by default**: the base install stays
77
+ PyYAML-only, and a default scan never loads the model runtime.
78
+
79
+ ```bash
80
+ # Opt into the heavier install once:
81
+ uv sync --extra semantic # or: pip install mcp-skill-scanner[semantic]
82
+
83
+ # Then scan with semantic detection enabled:
84
+ uv run mcpsec scan . --semantic
85
+ ```
86
+
87
+ The default embedder is a small, CPU-only ONNX sentence-transformer:
88
+
89
+ - **Model:** `all-MiniLM-L6-v2` (ONNX export `Xenova/all-MiniLM-L6-v2`), Apache-2.0.
90
+ - **Pinned revision:** `751bff37182d3f1213fa05d7196b954e230abad9` (files verified by SHA-256).
91
+ - **Corpus:** a curated, versioned set of malicious-intent anchors
92
+ (`src/mcpsec/corpus.py`, version `1`); the model revision and corpus version
93
+ are recorded on every `SKL-semantic-malicious` finding for reproducibility.
94
+
95
+ **To verify:** the similarity threshold (default `0.85`) and the corpus anchor
96
+ coverage are calibrated against synthetic probes, not a production corpus; they
97
+ should be re-tuned against real-world skill data before relying on this rule as
98
+ a CI gate.
99
+
100
+ ### Exit codes
101
+
102
+ | Code | Meaning |
103
+ |------|---------|
104
+ | 0 | Scan completed; no findings at or above `--fail-on`. |
105
+ | 1 | Scan completed; one or more findings at or above `--fail-on`. |
106
+ | 2 | Internal error (bad args, unreadable path, parse error). |
107
+
108
+ ## Rules
109
+
110
+ | Rule ID | Level | Detects |
111
+ |---------|-------|---------|
112
+ | `MCP-shell-unrestricted` | error | Server `command` is a shell interpreter (`sh`, `bash`, `zsh`, `dash`, `ksh`, `fish`, `csh`, `powershell`, `pwsh`, `cmd`…), including behind a launcher (`env bash`, `sudo sh`). |
113
+ | `MCP-allow-all` | error | `args` contains `--allow-all` / `--allow-everything` / `-A`. |
114
+ | `MCP-broad-network` | error | `--allow-net` with no host restriction (bare flag or empty value). |
115
+ | `MCP-inline-code` | warning | Interpreter runs code passed on the command line (`python -c`, `node -e`, `ruby -e`, `perl -e`, `php -r`). |
116
+ | `MCP-insecure-transport` | error | Remote MCP server uses plaintext `http://`. |
117
+ | `MCP-header-secret` | warning | A secret is stored inline in a remote server's `headers`. |
118
+ | `MCP-remote-no-auth` | note | Remote MCP server has no auth-bearing header. |
119
+ | `MCP-parse-ambiguous` | note | Server command cannot be statically resolved from the config. |
120
+ | `MCP-unreachable` | note | Remote endpoint refused the connection (`--online`). |
121
+ | `FLOW-shell-injection` | error | Tainted MCP tool input reaches a shell execution sink (Python taint analysis). |
122
+ | `FLOW-sql-injection` | error | Tainted input reaches a SQL query sink. |
123
+ | `FLOW-code-eval` | warning | Tainted input reaches `eval` / `exec` / `compile`. |
124
+ | `MCP-broad-filesystem` | warning | `--allow-read` / `--allow-write` at a root-level path (`/`, `~`, `C:\`, `C:/`) or with no path. |
125
+ | `SKL-shell-execution` | warning | Skill body invokes a shell with interpolated input (f-string / concatenation). |
126
+ | `SKL-remote-code-exec` | error | Skill body pipes remote content to a shell (`curl … \| sh`, `wget … \| bash`, `eval(fetch(…))`). |
127
+ | `SKL-obfuscated-command` | warning | Shell command decodes a `base64` payload (`base64 -d` / `--decode`). |
128
+ | `SKL-sql-concatenation` | error | SQL built by string concatenation/interpolation; parameterized `?` queries do not trigger. |
129
+ | `DEP-unpinned` | warning | A direct dependency whose version spec is unpinned (`*`, `latest`, `^`, `~`, empty). |
130
+ | `DEP-known-vulnerability` | error | Declared version has a known vulnerability in OSV.dev (`--online`). |
131
+ | `DEP-outdated` | warning | Declared version is behind the latest published one (`--online`). |
132
+ | `DEP-unknown` | warning | Name closely resembles a popular package, a typosquat signal (`--online`). |
133
+ | `SKL-semantic-malicious` | warning | Skill prose is semantically similar to a known-malicious reference (opt-in via `--semantic`). |
134
+
135
+ ## LLM triage (optional)
136
+
137
+ `mcpsec triage` runs the same scan, then explains each finding and proposes a fix with a
138
+ **local** model (Ollama). It is advisory: findings, levels and the exit code are never
139
+ changed by the model ([ADR-0011](docs/adr/0011-llm-triage-advisory.md)).
140
+
141
+ ```bash
142
+ uv sync --extra agent # LangGraph + Langfuse
143
+ ollama pull qwen2.5-coder:3b
144
+ uv run mcpsec triage . --exclude tests/fixtures # text report
145
+ uv run mcpsec triage . --format sarif --output triage.sarif # SARIF + properties.triage
146
+ ```
147
+
148
+ How it works: a LangGraph graph fans out one branch per finding (`Send`); each branch loads
149
+ the file excerpt, asks the model for `explanation`, `fix` and `evidence`, and keeps the answer
150
+ only if the evidence line really exists in the file (one stricter retry, then a rule-based
151
+ explanation). The excerpt is passed as untrusted data, so instructions hidden in a scanned
152
+ skill cannot change the result. Set `LANGFUSE_BASE_URL`, `LANGFUSE_PUBLIC_KEY` and
153
+ `LANGFUSE_SECRET_KEY` to trace every node, model call (tokens) and `grounded` score in a
154
+ self-hosted Langfuse.
155
+
156
+ ## CI integration
157
+
158
+ The scanner is deterministic and offline, so it fits a CI gate. This repository
159
+ ships its own GitHub Actions workflow —
160
+ [`.github/workflows/ci.yml`](.github/workflows/ci.yml) — which lints (ruff), runs the
161
+ test suite, **scans this repository with mcpsec itself** and uploads the SARIF to the
162
+ Security tab, and runs a gitleaks secrets scan on every push and pull request.
163
+
164
+ To gate another project on `mcpsec` (actions pinned by commit SHA, as in this repo):
165
+
166
+ ```yaml
167
+ jobs:
168
+ mcpsec:
169
+ runs-on: ubuntu-latest
170
+ permissions:
171
+ contents: read
172
+ security-events: write # required to upload SARIF
173
+ steps:
174
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
175
+ with:
176
+ persist-credentials: false
177
+ - uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
178
+ - name: Scan for risky MCP configs and skills
179
+ run: uvx --from git+https://github.com/A-Djihadi/mcp-security-scanner mcpsec scan . --output results.sarif
180
+ - name: Upload SARIF
181
+ if: always()
182
+ uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
183
+ with:
184
+ sarif_file: results.sarif
185
+ ```
186
+
187
+ `uvx --from git+…` installs mcpsec in an isolated environment (pin it with `@<tag or sha>`
188
+ in real use); `uv run mcpsec` only works inside this repository.
189
+
190
+ The default `--fail-on error` makes `mcpsec` exit non-zero when an error-level
191
+ finding exists, which blocks the build.
192
+
193
+ ## License
194
+
195
+ [MIT](LICENSE) © 2026 Djihadi Ahamdy.
@@ -0,0 +1,37 @@
1
+ [project]
2
+ name = "mcp-skill-scanner"
3
+ version = "0.2.0"
4
+ description = "Static security scanner for MCP servers and AI agent skills."
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ dependencies = ["pyyaml>=6.0.3"]
8
+
9
+ [project.scripts]
10
+ mcpsec = "mcpsec.cli:main"
11
+
12
+ [project.optional-dependencies]
13
+ semantic = [
14
+ "onnxruntime==1.31.1",
15
+ "tokenizers==0.23.3",
16
+ ]
17
+ agent = [
18
+ "langgraph>=1.2,<2",
19
+ "langfuse>=5.1,<6",
20
+ ]
21
+
22
+ [build-system]
23
+ requires = ["uv_build>=0.13.6,<0.14.0"]
24
+ build-backend = "uv_build"
25
+
26
+ [tool.uv.build-backend]
27
+ module-name = "mcpsec"
28
+
29
+ [tool.ruff]
30
+ target-version = "py313"
31
+ extend-exclude = ["tests/fixtures"]
32
+
33
+ [dependency-groups]
34
+ dev = [
35
+ "pytest>=9.2.0",
36
+ "ruff==0.17.3",
37
+ ]
@@ -0,0 +1,42 @@
1
+ [project]
2
+ name = "mcp-skill-scanner"
3
+ version = "0.2.0"
4
+ description = "Static security scanner for MCP servers and AI agent skills."
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ dependencies = [
8
+ "pyyaml>=6.0.3",
9
+ ]
10
+
11
+ [project.scripts]
12
+ mcpsec = "mcpsec.cli:main"
13
+
14
+ [project.optional-dependencies]
15
+ semantic = [
16
+ "onnxruntime==1.31.1",
17
+ "tokenizers==0.23.3",
18
+ ]
19
+ # LLM triage (ADR-0011): LangGraph orchestration + Langfuse tracing.
20
+ agent = [
21
+ "langgraph>=1.2,<2",
22
+ "langfuse>=5.1,<6",
23
+ ]
24
+
25
+ [build-system]
26
+ requires = ["uv_build>=0.13.6,<0.14.0"]
27
+ build-backend = "uv_build"
28
+
29
+ [tool.uv.build-backend]
30
+ module-name = "mcpsec"
31
+
32
+ [dependency-groups]
33
+ dev = [
34
+ "pytest>=9.2.0",
35
+ "ruff==0.17.3",
36
+ ]
37
+
38
+ [tool.ruff]
39
+ target-version = "py313"
40
+ # Fixtures are deliberately vulnerable or partial code: their line/column
41
+ # positions are asserted by tests, so they are never reformatted or linted.
42
+ extend-exclude = ["tests/fixtures"]
@@ -0,0 +1,3 @@
1
+ """mcpsec — static security scanner for MCP servers and AI agent skills."""
2
+
3
+ __version__ = "0.1.0"