Find shadow MCP servers your developers wired up
DVARA, the AI governance platform, governs every MCP call that goes through the gateway. A server that a developer adds to Cursor, VS Code or Claude Desktop and calls directly never reaches the gateway, so the gateway cannot report it. OWASP lists these as MCP09, Shadow MCP Servers.
Those servers are written down, though: in the client's MCP config file, or as a package in a project's dependencies. DVARA ships a small scanner that reads those files, and a Flightdeck page that compares what it found with the servers you registered.
Not included in DVARA Open Source.
The scanner
mcp_scan.py is one Python file. Download it from the MCP Shadow Servers
page in DVARA Flightdeck (see below). It needs Python 3.9 or later and nothing
else, and it is short enough to read before you run it.
- It reads only the files and folders you name. With
--homeit also reads the known MCP client config files in your own home folder, and nothing else there. - It makes no network connection and runs nothing it finds.
- It needs no DVARA account or key, so it works before DVARA is installed.
- It writes nothing but its report.
DVARA never scans anyone's machine or network. A person runs the scanner and decides what to share.
It knows the config files of Claude Code, Claude Desktop, Cursor, VS Code,
Windsurf, Cline, Gemini CLI, Zed and Codex. It also lists MCP packages in
package.json, pyproject.toml and requirements.txt.
# A repository
python3 mcp_scan.py path/to/repo --out mcp-scan.json
# The MCP clients on your own machine
python3 mcp_scan.py --home --out mcp-scan.json
What the report keeps
The report keeps names, never secrets:
| Kept | Never kept |
|---|---|
| The server's name, its client and its file | Environment variable values |
| The package, image or command it runs, or the URL's host and path | Header values, such as tokens |
| The transport | Command arguments |
| The names of the variables and headers it sets | A URL's query, user name or password |
Compare it with what you registered
Open MCP Shadow Servers in Flightdeck and upload the report. The same page offers the scanner itself as a download.
- Platform roles: MCP → Shadow Servers, then choose the workspace.
- Workspace roles (admin, developer and viewer): Agents → MCP Shadow Servers. The report is compared with your own workspace.
Give your gateway's address too, so that servers reached through it count as governed. Each server is then shown as:
| Status | Meaning | What to do |
|---|---|---|
| Shadow | No server registered to the workspace runs or calls it | Register it, or remove it from the client |
| Not approved | It is registered, but quarantined, suspended or disabled, and the client calls it directly | Remove it from the client until it is approved |
| Called directly | It is registered and active, but the client calls it directly | Point the client at the gateway |
| Governed | The client calls it through the gateway | Nothing |
Servers are compared by what they run or call, not by the name a developer gave
them. npx -y @acme/server@1.2.0 and npx @acme/server are the same server.
The upload is recorded in the audit log with its counts and the shadow servers' names. Flightdeck does not store the report.
Use it as a CI gate
Download registered list on the same page saves your workspace's registered servers as a file: what each runs or calls, and its status. It holds no credentials. The scanner can then fail a build that adds an unregistered server, still without network access:
python3 mcp_scan.py . --registered mcp-registered.json --fail-on SHADOW,UNAPPROVED
The run exits with code 1 and names each server that has one of the listed statuses. A server the client config turns off does not fail it.
What it does not find
- A server nobody wrote down. One started by hand, or by a client whose config the scanner does not know, is not found.
- Machines nobody scanned. The report covers the files it was given.
- A local script at another path. It is matched by its path, so the same script elsewhere shows as shadow. That errs on the side of a second look.
- Version differences. A package is matched by name; the version each side names is shown, not judged.