Choose your setup

Shadow MCP: finding unsanctioned MCP servers on your team's laptops

Shadow MCP is a server living in a developer's own Claude Desktop, Cursor or VS Code config, holding a real credential, that nobody in security approved or even knows is running.

By
Dan Flanagan
Updated
October 14, 2026
Checked against
VS Code's MCP configuration reference and the Model Context Protocol specification's Security Best Practices page (modelcontextprotocol.io), October 2026
Reading time
6 min

New to MCP? What is an MCP server? covers the basics first.

Shadow MCP is what you get when a developer adds an MCP server to their own AI tool's config file, Claude Desktop, Cursor or VS Code, to get something done, and nobody outside their own laptop knows the server exists. It's shadow IT again. Nobody meant any harm, but there's now a credential and a tool that nobody responsible for security signed off on.

How a server ends up unsanctioned

Adding an MCP server rarely goes through a review, because most clients don't ask for one. A developer finds a server for what they need, pastes a config block from its README into their client's settings, and it works. Nobody else ever hears about it.

That's how MCP clients work. They store servers in a config file on the developer's machine, read it at startup, and give the AI app every tool the server offers. Nothing in that flow goes past IT or into a shared list of what's installed where.

Take a concrete case. A developer wants an agent in Cursor to answer questions against a staging database, finds a Postgres MCP server on npm, and adds it to the project's .cursor/mcp.json with the connection string in the env block. It works well enough that they leave it running after the task is done. Three weeks later that file, with the real DATABASE_URL, is still read every time anyone opens the project in Cursor, including anyone who pulls the repo without knowing a database credential came with it.

Why it matters

An unsanctioned server brings three problems with it.

Plaintext keys sit in a file, not a vault. A connection string or API key pasted into env in a config file sits in plaintext, readable by anything with filesystem access, including the agent itself if it has a filesystem tool. Keeping credentials out of an agent's reach covers why that's worse than it sounds. The key doesn't even need an attacker. A tool call that echoes an error message can put it in the model's context.

The package is unvetted. Installing an MCP server is installing a dependency that gets your credentials, usually from npm, PyPI or a GitHub repo, usually by whoever found it first. Malicious MCP servers covers the postmark-mcp case: a package that impersonated a real vendor's server and worked correctly for weeks before a later version added a hidden blind copy on every email it sent. Shadow MCP doesn't make that more likely on its own, but it means nobody confirmed the publisher or pinned a version.

The server's full scope is on by default. An MCP server offers every tool it has to the client that connects. The Postgres server above almost certainly bundles writes next to reads, so the agent can run anything the server allows, not just the questions the developer wanted answered. That's what excessive agency describes: capability the task doesn't need, sitting there until a bad prompt or an injected instruction uses it.

None of this needs bad judgment from the developer. For most teams, a config file is the only control there is.

Where to look

A client's own config file is the first place a shadow server shows up, and the locations are documented.

Where MCP servers get configured, by client
ClientScopeFile
Claude DesktopEvery project, one filemacOS: ~/Library/Application Support/Claude/claude_desktop_config.json. Windows: %APPDATA%\Claude\claude_desktop_config.json. Linux: ~/.config/Claude/claude_desktop_config.json
Claude CodeProject, shared through version control.mcp.json in the project root
Claude CodeUser or local, not shared~/.claude.json
CursorEvery project, the developer's own~/.cursor/mcp.json
CursorOne project, shared through version control.cursor/mcp.json in the project root
VS CodeWorkspace, shared through version control.vscode/mcp.json in the project root
VS CodeUser profile, the developer's ownOpened with the command MCP: Open User Configuration

Verify these against the client's own docs before you rely on them, since a config format and its default location can change between versions. VS Code's own MCP configuration reference is a useful cross-check: it lists the exact paths it reads to auto-discover servers already configured in Claude Desktop, Cursor and Windsurf, because VS Code itself needed to know where to look.

A sweep across a team's laptops means checking each path on each machine, realistic for a handful of developers and not realistic by hand past that. Look for a server nobody recognizes, a key sitting directly in the env block instead of a reference, and a .mcp.json or .cursor/mcp.json committed to the repo with a real credential in it.

Moving to a sanctioned path without blocking developers

The failure mode on the other side is just as real: lock down MCP servers with a slow approval process, and developers go around it, so you end up seeing even less. The goal is to make the sanctioned path the fast one.

An allowlist developers can request against. A short list of approved servers, reviewed once, lets a developer self-serve instead of waiting on a one-off review each time. It needs an owner and a fast way to add to it, or people route around it the same as before.

A central point the credential goes through. Instead of a key living in each developer's config, a gateway between agents and servers holds the credential and attaches it to a call at request time. The config a developer edits still exists, but it points at the gateway instead of carrying the secret. MCP permissions and local vs. remote MCP servers cover the tradeoffs of putting something in that path.

Per-tool grants instead of per-server access. Once calls go through one place, access can be scoped to the tools a person or an agent actually needs. "Someone added a Postgres server" becomes "someone added a Postgres server, and it can only run reads," which is a different incident.

Most teams end up with both: the allowlist sets what's approved, and the gateway makes the approved path easier to use than pasting a config block. Neither makes a malicious or compromised server harmless, and neither stops a model from being manipulated into misusing a tool it was legitimately granted. What is an MCP gateway? and MCP API keys vs. OAuth go into each piece further.

Recommendation

If you're a handful of developers on one or two services, start with the allowlist and a quarterly sweep of the config paths above. A short list plus a recurring check catches most of what shadow MCP creates, and a gateway isn't worth the overhead yet.

If you have more developers, more services, or any server touching production data, move the credential off laptops before anything else. A manual sweep can't do that part for you.

Statio's desktop app reads the same local client configs described above and lists each server it finds as Managed or External, so you can run the sweep on demand instead of by hand on every laptop. That inventory view is on the Team plan and up. Once a server is behind the gateway, per-tool permissions narrow what it can do, and each agent gets its own identity instead of a person's key. Calls land in the audit trail. The MCP security checklist has a question on whether you can list every server running on your team's machines, with a fix if the answer is no. If you're comparing gateways rather than picking Statio specifically, choosing an MCP gateway covers the field.

Next

Questions

It's the same pattern applied to one specific thing: an AI client's own config file. Someone solves a problem without a review that would have slowed them down. The fix is the same too: make the approved path at least as fast as the unapproved one.

No. Deleting the config entry stops the client from loading the server, but it does nothing to the credential itself. If the key was ever in that file, rotate it. The config file can't tell you what the server, or anything else that read the file, already did with the key.

Only for remote servers reached over the network, and only if you know which domains to watch for. A local (stdio) server runs as a process on the developer's own machine and talks to the AI client over standard input and output, not a network connection a firewall sees.

Not if the sanctioned path is fast enough that going around it isn't worth the trouble. A short allowlist that developers can request against, reviewed on a real turnaround time, keeps the server review without pushing people back toward the config file.