mcplint
Local-first, CI-native security scanner for MCP servers. OWASP MCP Top 10 rules, a lockfile for rug-pull detection, and an AIBOM export. It never executes your MCP servers.
uvx mcplint-sec scan
PyPI distribution is
mcplint-sec(themcplintname collides with an existing project); it installs themcplintcommand. Withuvx, invoke it by distribution name:uvx mcplint-sec ….
Why
MCP went from a few hundred servers to a 10,000+ server ecosystem — and the security model did not keep up:
- ~40% of internet-exposed MCP servers have no authentication (Censys, 2026)
- A single compromised MCP server reaches a 78% attack success rate when five servers share one agent (arXiv 2601.17549)
CVE-2025-6514inmcp-remote(CVSS 9.6) affected a package with 437k+ downloads- Registries accepted typosquatted MCP servers; tool descriptions are mutable after approval ("rug pulls")
Most scanners run on your machine and hand your tool descriptions to a vendor API. mcplint scans the configs in your repo, in CI, with nothing leaving your environment.
Quickstart
# scan the current repo
uvx mcplint-sec scan
# also scan user-level client configs and skills
uvx mcplint-sec scan --home
# pin server fingerprints, detect drift in CI
uvx mcplint-sec lock
uvx mcplint-sec lock --check
# CycloneDX AIBOM of every MCP server
uvx mcplint-sec inventory -o aibom.json
# probe a RUNNING gateway for missing authentication (read-only, loopback by default)
uvx mcplint-sec gate
uvx mcplint-sec gate https://gateway.internal:4000 --allow-host
# what do the rules mean?
uvx mcplint-sec rules list
uvx mcplint-sec rules explain MCP004
# scaffold a GitHub Actions workflow + starter config
uvx mcplint-sec init
Exit code is 1 when a finding at --fail-on severity (default high)
exists — drop it into CI as-is.
What it scans
| Input | Examples |
|---|---|
| MCP configs | .mcp.json, mcp.json, .cursor/mcp.json, .vscode/mcp.json, opencode.json[c], .codex/config.toml, cline_mcp_settings.json, ~/.claude.json, ~/.codeium/windsurf/mcp_config.json, ~/.gemini/settings.json |
| Instruction / skill files | SKILL.md, AGENTS.md, CLAUDE.md, .cursorrules, .windsurfrules, .github/copilot-instructions.md, .cursor/rules/*.mdc |
Scans recurse into subdirectories (node_modules, virtualenvs and build
outputs are skipped), so monorepos work out of the box.
Rules
| Rule | Severity | OWASP MCP Top 10 | Checks |
|---|---|---|---|
| MCP001 | critical | MCP01 Token Mismanagement | hardcoded secrets in env/args/headers |
| MCP002 | medium | MCP01 | unsafe config file permissions |
| MCP003 | medium | MCP04 Supply Chain | unpinned npx/uvx/pipx packages |
| MCP004 | high | MCP04 Supply Chain | typosquat/lookalike package names |
| MCP005 | high | MCP07 Auth | remote endpoint over plain http:// |
| MCP006 | medium | MCP07 Auth | remote endpoint with no auth material |
| MCP007 | medium | MCP02 Scope Creep | filesystem server scoped to /, $HOME, ... |
| MCP008 | high | MCP05 Command Execution | shell / command-execution servers |
| MCP009 | high | MCP03 Tool Poisoning / MCP06 Intent Flow Subversion | prompt-injection indicators in instructions |
| MCP010 | critical | MCP03 Tool Poisoning / MCP06 Intent Flow Subversion | zero-width / bidi unicode (hidden text) |
| MCP011 | medium | MCP03 Tool Poisoning | cross-config server name shadowing |
| MCP012 | high | MCP03 Tool Poisoning | lockfile drift (rug-pull detection) |
| MCP013 | high | MCP04 Supply Chain | pinned packages matching OSV advisories (--online) |
Rules are data: plain YAML in src/mcplint/rules_data/.
Bring your own with --rules-dir ./my-rules.
Coverage: the 13 rules map to 7 of the 10 OWASP MCP Top 10 categories; MCP08 (audit & telemetry), MCP09 (shadow servers) and MCP10 (context over-sharing) are runtime and operational risks outside the reach of static config scanning.
Runtime gate
Static rules can tell you a config looks right. mcplint gate tells you
whether a gateway that is already running actually enforces
authentication. It sends one small, read-only request per known failure class —
derived from public CVEs and advisories — and checks that every one of them is
denied.
- Read-only. No tool calls, no state changes: the battery only asks "does this endpoint reject anonymous callers?".
- Loopback by default. Anything that is not
localhost/127.0.0.1requires--allow-host(confirming the gateway is yours). - Rules as data. Profiles live in
src/mcplint/gate_data/; bring your own with--profiles-dir.
uvx mcplint-sec gate # http://localhost:4000
uvx mcplint-sec gate https://gateway.internal # + --allow-host
uvx mcplint-sec gate --json --fail-on high # CI-friendly
| Probe | Severity | Derived from | Checks |
|---|---|---|---|
| GATE001 | critical | CVE-2026-59822 | MCP /mcp accepts a fabricated Authorization bearer |
| GATE002 | critical | CVE-2026-59822 | MCP /mcp accepts an invalid x-litellm-api-key |
| GATE003 | high | — | MCP /mcp answers anonymous callers at all |
| GATE004 | high | — | Legacy /sse endpoint answers anonymous callers |
| GATE005 | high | CVE-2026-42271 | /mcp-rest/test/connection reachable without credentials |
| GATE006 | high | — | MCP management API reachable without credentials |
| GATE007 | medium | CVE-2026-49468 | Management route authenticates from a spoofed Host header |
Exit codes: 0 clean, 1 finding at --fail-on severity or above, 2
operational error (bad profile, unreachable target, non-loopback target
without --allow-host). Add it to your deploy pipeline and re-run it after
every gateway upgrade.
Lab-verified against real LiteLLM releases: patched 1.100.0 denies every
probe; pre-fix 1.83.14 errors instead of denying on the MCP routes —
see research/gate-lab-verification.md.
Authenticated checks (gate --auth)
The anonymous battery answers "can strangers get in?". The authenticated mode answers the harder question for shared gateways: does this key get exactly what it should? — with a test key, still read-only.
export LITELLM_TEST_KEY=sk-... # dedicated test key, not a person's
uvx mcplint-sec gate --auth expectations.yaml https://gateway.internal --allow-host
# or keep every secret in one uncommitted file instead of exporting:
uvx mcplint-sec gate --auth expectations.yaml --env-file .env.local --allow-host
Expectations are data (see
gate_data/auth-expectations.example.yaml):
| Check | Configuration | Severity |
|---|---|---|
| AUTH001 | tools matching forbidden_tools patterns are visible to the key |
high |
| AUTH002 | tools outside the strict expect_tools allowlist are visible |
medium |
| AUTH003 | x-mcp-servers scoping not enforced (forbidden_servers) |
high |
| AUTH004 | opt-in read_probe returned data for an object the test user cannot access |
critical |
| AUTH005 | an expected tool is missing (config drift) | low |
| AUTH006 | read_probe denied although the user should have access (expect: allow) |
medium |
Per-user gateways (Confluence-style) can forward the test user's upstream token
with upstream_headers (also env-referenced, never literal):
upstream_headers:
x-mcp-confluence-authorization: env/ATLASSIAN_USER_TOKEN
If a gateway denies /mcp because its route patterns only match /mcp/, gate
retries the canonical path automatically.
Safety: the key and every upstream_headers value are read from environment
variables only (literals in the file are rejected) and are never printed; no
tool calls happen unless read_probe is explicitly configured; returned content
is never echoed (redacted in evidence). --env-file FILE is a convenience for
injecting the key and per-server tokens (existing environment variables win;
keep that file out of version control). Use a dedicated test key that maps to a
test user.
GitHub Actions
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v4
- uses: dtduc-git/mcplint@v0.2.0
with:
fail-on: high
Findings show up as annotations and in the repo's code-scanning tab (SARIF).
Or scaffold this workflow and a starter config with uvx mcplint-sec init.
Design principles
- Never executes your MCP servers. Scanning is static by default; running
arbitrary server commands in CI is not acceptable. The one active command is
gate: read-only HTTP probes against a target you own, no tool calls. - Nothing leaves your machine unless you opt in with
--online(OSV CVE lookups only) or explicitly pointgateat a remote host. - Pin and diff.
.mcplint.lock.jsonfingerprints every server (salted hashes for env values) so post-approval changes are visible ingit diff. - Rules as data. YAML + a small, tested check engine — contributions do not need to touch the scanner core.
- Non-goals: no gateway, no proxy, no runtime traffic monitoring, no SaaS.
Research
State of MCP configs in the wild — an
aggregate scan of 1,210 public MCP configs from 1,197 repositories (56.5% have
at least one finding). Methodology and the reproducible script
(scripts/ecosystem_scan.py) are included.
Read the write-up: We scanned 1,210 MCP configs on GitHub. 56% have a security finding.
Development
uv sync --all-groups
uv run pytest
uv run ruff check .
uv run mcplint scan fixtures/vulnerable-repo --fail-on none
License
Apache-2.0
Release files for mcplint-sec 0.3.5
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| mcplint_sec-0.3.5.tar.gz | 88.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mcplint_sec-0.3.5-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 143.9 kB
Release files / mcplint_sec-0.3.5.tar.gz
| Download URL | mcplint_sec-0.3.5.tar.gz |
|---|---|
| Size | 88.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
009c38c94dd649ce5c9bda30515632d2af58959a580477208e09f80e39077a66
|
|
BLAKE2b-256 checksum How to use checksums |
d46d9f79a898a290f792c2ca0f8e2c3c8fe8ec84b6786145eb604d191c529e3e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.16 {"installer":{"name":"uv","version":"0.12.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / mcplint_sec-0.3.5-py3-none-any.whl
| Download URL | mcplint_sec-0.3.5-py3-none-any.whl |
|---|---|
| Size | 55.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7b022f303467e6ad23f311faa20e173e2bac8e4c8848cc13ddaccad3c47737e8
|
|
BLAKE2b-256 checksum How to use checksums |
67486190577b6edc8a3a49eefe20162d6ed290a700e0bc87ee411669d45aa874
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.16 {"installer":{"name":"uv","version":"0.12.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|