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.
Use inside Claude Code
This repo is also a Claude Code plugin marketplace. Add it once, then install
the mcplint plugin (a skill that runs the pinned scanner):
/plugin marketplace add dtduc-git/mcplint
/plugin install mcplint@mcplint
The plugin is skill-only: it runs no code of its own and instructs Claude to
invoke uvx --from mcplint-sec==0.6.0 mcplint … locally — same offline
guarantees as the CLI.
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 |
Installed Claude Code plugins (--home) |
MCP configs (.mcp.json, inline mcpServers in .claude-plugin/plugin.json) and skills from the active installs in ~/.claude/plugins/installed_plugins.json, plus synced/ and local/. Not walked directly: the marketplaces/ tree (only manifest-listed installs are scanned); orphaned cache versions (.orphaned_at) are skipped. For cache installs, the plugin's own top-level tests, test, fixtures, examples, example, docs directories are skipped — content deeper inside the plugin (a skill named docs, for example) is still scanned |
| Instruction / skill files | SKILL.md, AGENTS.md, CLAUDE.md, .cursorrules, .windsurfrules, .github/copilot-instructions.md, .cursor/rules/*.mdc |
mcpServers declared as a file path inside plugin.json ("./config/servers.json")
is not followed yet — only inline objects and .mcp.json files are scanned.
Scans recurse into subdirectories (node_modules, virtualenvs and build
outputs are skipped), so monorepos work out of the box. .claude-plugin/plugin.json
is scanned in repository scans too (inline mcpServers is part of a plugin's MCP
surface), so lock and inventory output can change for repos that ship a plugin
manifest — regenerate the lockfile after upgrading. The same applies to Codex
configs that use http_headers: those headers are now part of the server
fingerprint, so regenerate the lockfile after upgrading to avoid a spurious
lockfile-drift finding.
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) |
| MCP014 | low | — (MCP lifecycle) | deprecated HTTP+SSE transport — migrate to Streamable HTTP |
| MCP015 | medium | MCP02 Scope Creep | skill allowed-tools auto-approves wildcards or an unrestricted shell |
Rules are data: plain YAML in src/mcplint/rules_data/.
Bring your own with --rules-dir ./my-rules.
Coverage: the 14 OWASP-mapped rules cover 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. MCP014 tracks the MCP spec deprecation lifecycle rather than an OWASP entry: the HTTP+SSE transport was deprecated in the 2025-03-26 revision and reclassified under the 2026-07-28 lifecycle policy.
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 a small, read-only battery of requests (one or two
per failure class) and expects each one to be denied. Five of the eight probes
are derived from a public CVE or advisory and cite it; three are baseline
hygiene checks.
Why this exists, in numbers:
-
CVE-2026-59822 (LiteLLM MCP auth bypass) is the first MCP flaw on CISA's Known Exploited Vulnerabilities catalog (2026-09-02) — the "Bearer a" one-character token was enough to open an MCP session, and it was chained to command injection and cryptominers in the wild.
-
Wiz Research scanned 3,074 public LiteLLM instances: 9.6% accepted the default master key
sk-1234or required no authentication at all; Censys counted 12,500+ MCP services reachable from the internet in April 2026. -
Read-only. No tool calls, no state changes: the battery only asks "does this endpoint reject anonymous callers?". Anonymous-battery findings carry the request trace and status — response bodies (tool names, upstream URLs, stack traces) stay out of that report.
-
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. A step markedexpect: denyis a negative control (it must be rejected before the probe proceeds), and a probe markedrequire: toolsonly fires when a 2xx actually carries a tools inventory. Unknown values are rejected at load time.
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 |
| GATE008 | high | CVE-2026-52869 | MCP /mcp served tools on a never-issued Mcp-Session-Id (with negative control) |
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 produces a
report with no findings (the legacy /sse route is absent, so that probe is
inconclusive, not a denial). Pre-fix 1.83.14 errors instead of denying on
the MCP routes. GATE008 landed after the lab run and has not been exercised
against a real release yet —
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; tool-call
content is never echoed (AUTH004/AUTH006 evidence is redacted — the tool
inventory the key can see is printed by design). --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.6.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. The fingerprint hashes commands and URLs without a secret store: treat a committed lockfile as sensitive if your configs may embed credentials. - 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
Metadata
Release files for mcplint-sec 0.6.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| mcplint_sec-0.6.0.tar.gz | 119.5 kB | Without provenance | Details for mcplint_sec-0.6.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| mcplint_sec-0.6.0-py3-none-any.whl | Python 3 | none | any | Details for mcplint_sec-0.6.0-py3-none-any.whl |
Total release size: 186.3 kB
Release files / mcplint_sec-0.6.0.tar.gz
| Download URL | mcplint_sec-0.6.0.tar.gz |
|---|---|
| Size | 119.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8ec2393d1d1418628c4265a1fb68adf87a2caa48ce1f1f478fecd9582fc2556b
|
|
BLAKE2b-256 checksum How to use checksums |
d8bd30909611eea772f0ed632abcedb75a825e0c8f428128343d9afeb8647367
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.22 {"installer":{"name":"uv","version":"0.12.22","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.6.0-py3-none-any.whl
| Download URL | mcplint_sec-0.6.0-py3-none-any.whl |
|---|---|
| Size | 66.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c130e01dc7db195d640e26407fb39c828d5fac18da266a440d108658167081a8
|
|
BLAKE2b-256 checksum How to use checksums |
f5747d5706a8422bf2c015311eff528c190ea346f47434e0052fb54605de2f07
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.22 {"installer":{"name":"uv","version":"0.12.22","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}
|
Security for mcplint-sec 0.6.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.