Skip to main content

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.

CI PyPI License M8ven Live Monitored GitHub Marketplace

uvx mcplint-sec scan

PyPI distribution is mcplint-sec (the mcplint name collides with an existing project); it installs the mcplint command. With uvx, 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-6514 in mcp-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-1234 or 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.1 requires --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 marked expect: deny is a negative control (it must be rejected before the probe proceeds), and a probe marked require: tools only 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

  1. 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.
  2. Nothing leaves your machine unless you opt in with --online (OSV CVE lookups only) or explicitly point gate at a remote host.
  3. Pin and diff. .mcplint.lock.json fingerprints every server (salted hashes for env values) so post-approval changes are visible in git diff. The fingerprint hashes commands and URLs without a secret store: treat a committed lockfile as sensitive if your configs may embed credentials.
  4. Rules as data. YAML + a small, tested check engine — contributions do not need to touch the scanner core.
  5. 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)

Source distribution for mcplint-sec 0.6.0
File Size Uploaded
mcplint_sec-0.6.0.tar.gz 119.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for mcplint-sec 0.6.0
File Interpreter ABI Platform
mcplint_sec-0.6.0-py3-none-any.whl Python 3 none any Details

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}

Release history Release notifications | RSS feed

This release

0.6.0 This release

2 release files

0.5.0

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.5

2 release files

0.3.4

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page