Discover and risk-grade the MCP servers actually present on this machine. Local-first shadow-MCP inventory.
Project description
shadow-mcp
Discover and risk-grade the MCP servers actually present on this machine.
Most MCP security tooling assumes you already have a list of servers to audit.
On a real developer machine you don't: servers are scattered across Claude Code,
Codex, Claude Desktop, project-local .mcp.json files, DXT extensions, and live
processes that bind no port. shadow-mcp finds them first, then grades them.
This is the local-first answer to OWASP MCP09:2025 — Shadow MCP Servers.
What it does
discover -> inventory -> risk-grade -> report
- Discover (read-only) every place an MCP server is declared or running:
Claude Code (
~/.claude.json, user + project scope),claude mcp list(catches remote + plugin servers no file contains), Codex (~/.codex/config.toml+ profiles), project.mcp.json, Claude Desktop config + DXT extension manifests, and the live process table. - Inventory: merge sightings into one entry per logical server, even when a
server appears under different names across hosts (
personal-opsvspersonal_ops), tracking every provenance. - Risk-grade by delegating to the existing engines rather than reimplementing them:
- Report: a ranked terminal table, a machine-readable JSON inventory, or markdown — plus a Shadow & attention section for the deltas that matter (running-but-unconfigured, broad blast radius, capable-but-ungraded).
The risk model and its OWASP mapping live in docs/risk-model.md.
Install
uv sync # installs deps incl. MCPAudit as a local editable engine
shadow-mcp grades against your local checkouts of MCPAudit (../MCPAudit) and
mcp-trust (../mcp-trust/registry.db). Override with SHADOW_MCP_MCPTRUST_DB
or --registry-db.
Use
uv run shadow-mcp scan # full pipeline, terminal report
uv run shadow-mcp scan --json out.json # machine-readable inventory
uv run shadow-mcp scan --format markdown # markdown report
uv run shadow-mcp discover # inventory only, no grading
uv run shadow-mcp sources # per-collector counts
uv run shadow-mcp grade-missing # A-F for servers the registry hasn't scanned
uv run shadow-mcp deep-scan cost-tracker # connect to a server, grade its real tools
Useful flags: --no-processes (skip the live process scan), --no-cli (skip
claude mcp list), --no-mcpaudit (inventory + mcp-trust only), --home PATH
(point discovery at a fixture tree).
Static vs connected grading
By default grading is static (config-only): no server is spawned, so grades reflect what's visible in the config. That's safe but coarse — a server's real capability only shows once you connect and list its tools.
shadow-mcp scan --connect (or deep-scan [names...]) spawns each stdio
server and enumerates its real tools, delegating to MCPAudit's connected engine
for a capability grade that actually differentiates (a filesystem server jumps
from a static A to a connected D). This is opt-in because connecting
executes the server; remote endpoints are never spawned (that's the network-scan
tier), and a server that needs real secrets to start falls back to its static
grade.
Development
uv sync # dev tools + grading engines (the default groups)
uv run pytest # full suite (61 + engine-backed tests)
uv run ruff check . # lint
The grading engines are an optional engines dependency-group, resolved to your
local checkouts of ../MCPAudit and ../mcp-trust via [tool.uv.sources]. The
tool degrades to discovery-only without them (engine-backed tests skip cleanly),
so CI installs without them:
uv sync --no-group engines # discovery + local OWASP layer only (what CI runs)
Safety
- Read-only discovery. Collectors parse configs and list processes; nothing
they find is ever mutated. (
--connect/deep-scanis the one path that executes servers, and only when you explicitly ask.) - Secrets stay out. We record env variable names (to flag secret-bearing
servers per MCP01) but never their values. A captured inventory still contains
real local paths and hostnames, so treat
*.inventory.jsonas private (it is git-ignored by default).
Use as an MCP server
shadow-mcp can serve its own inventory tools as an MCP server so an agent can query your local MCP surface without leaving the conversation.
Tools
| Tool | Description |
|---|---|
scan_local |
Full pipeline (discover → inventory → grade → report). Returns JSON. |
discover_local |
Inventory every MCP server without grading. Returns JSON. |
deep_scan |
Grade only the named servers (static, no spawning). Accepts names: list[str]. Returns JSON. |
list_sources |
Per-collector source counts from a discover run. Returns JSON. |
Run the server
# directly from a local checkout
shadow-mcp mcp-serve
# via uvx (once published to PyPI)
uvx shadow-mcp mcp-serve
LOCAL only. The MCP server never connects to hosted MCP endpoints — all
grading is static (config-based). connect=False is enforced unconditionally;
no server is ever spawned from an MCP tool call.
Scope
This is the local-first tool: it inventories one machine from its configs
and processes. A later network-scan expansion (probing hosts/ports for remote
MCP endpoints, org-wide fleet inventory, typosquat-distance provenance checks)
is deliberately out of scope here — see the bottom of docs/risk-model.md and
the project notes for what that would add.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file shadow_mcp-0.1.0.tar.gz.
File metadata
- Download URL: shadow_mcp-0.1.0.tar.gz
- Upload date:
- Size: 46.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9e27494b350389b08e5d9530fa820e74bd042bb4a52cea4211fb2e8ba314f963
|
|
| MD5 |
72d3c98c25d555b70706e0738662766d
|
|
| BLAKE2b-256 |
226f886717db2aeef2a3f48bd3a48c04ea0b1e484e49c839918b4c4b581e35f5
|
Provenance
The following attestation bundles were made for shadow_mcp-0.1.0.tar.gz:
Publisher:
publish.yml on saagpatel/shadow-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
shadow_mcp-0.1.0.tar.gz -
Subject digest:
9e27494b350389b08e5d9530fa820e74bd042bb4a52cea4211fb2e8ba314f963 - Sigstore transparency entry: 1999695789
- Sigstore integration time:
-
Permalink:
saagpatel/shadow-mcp@c880c5f760e463a230f32828024bd0f4badd6eaa -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/saagpatel
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c880c5f760e463a230f32828024bd0f4badd6eaa -
Trigger Event:
push
-
Statement type:
File details
Details for the file shadow_mcp-0.1.0-py3-none-any.whl.
File metadata
- Download URL: shadow_mcp-0.1.0-py3-none-any.whl
- Upload date:
- Size: 42.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e4da598900600e779dd8a52bf80a19dc02046467bed52433f2c7f68b22a3e3db
|
|
| MD5 |
a4f0a41789810d3ab00c156199e4f4fb
|
|
| BLAKE2b-256 |
55c8d17459e5dacb71bcf8fcea2de7dd0c46c0d88db78df564542370a266f65b
|
Provenance
The following attestation bundles were made for shadow_mcp-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on saagpatel/shadow-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
shadow_mcp-0.1.0-py3-none-any.whl -
Subject digest:
e4da598900600e779dd8a52bf80a19dc02046467bed52433f2c7f68b22a3e3db - Sigstore transparency entry: 1999695856
- Sigstore integration time:
-
Permalink:
saagpatel/shadow-mcp@c880c5f760e463a230f32828024bd0f4badd6eaa -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/saagpatel
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c880c5f760e463a230f32828024bd0f4badd6eaa -
Trigger Event:
push
-
Statement type: