# Securing MCP servers for coding agents

> An MCP server runs with your privileges or holds your OAuth token. See what is configured, what to approve, and how to scope MCP servers in Claude Code.

Canonical: https://ranwhat.com/guides/mcp-server-security-coding-agents
Last updated: 2026-10-09

An MCP server is code you run, or an account you hand over, and the agent decides when to call it. Know what each one can reach before you approve it.

```
$ uvx ranwhat reach
```

[Install](https://ranwhat.com/install)

Checked against Claude Code's documentation, the MCP specification's security guidance, the research under Sources, and ranwhat's source. Published 9 October 2026.

## What an MCP server can reach

Claude Code does not sandbox an MCP server for you. Anthropic reviews connectors against its listing criteria before they appear in its directory, but does not security-audit or manage any MCP server, and says so on its security page.

- **Local (stdio)**: Claude Code starts a command you configured, such as `npx` or `uvx` plus a package, and talks to it over standard input and output. The MCP specification's security guidance says plainly that a local server can run any command with the client's privileges. In practice that means your files, your shell environment and your network.
- **Remote (HTTP)**: Claude Code connects to a URL. The server runs elsewhere, but it acts on a service with an OAuth grant or header you gave it, and it receives whatever the agent sends in a tool call. Claude Code stores OAuth sign-ins per endpoint, and `claude mcp remove` deletes the tokens for that server.
- **Its environment**: A server's `env` block is whatever you put in it. A token with write access to every repository you own is what that server holds, whether or not it ever needs it.
- **Its words**: The server also supplies the text the model reads: tool names, tool descriptions and every result. The model treats it as context, and you rarely see all of it.

The permission prompt for a tool call is a check on each call, not on the code behind it. A server you approved once keeps the access you gave it. For the same reasoning applied to the credentials themselves, see [token scopes for AI agents](https://ranwhat.com/guides/ai-agent-token-scopes).

## The risks that are documented

- **Tool poisoning**: Invariant Labs named this in April 2025: instructions hidden in a tool's description that the model reads and you usually do not. In its demonstration against Cursor, a tool that claimed to add two numbers told the agent to read `~/.cursor/mcp.json` and an SSH private key and pass them as an argument. The confirmation dialog showed a shortened tool name, not the full input. The authors said any client that does not show and validate descriptions could be affected.
- **Changed later**: Invariant also described a rug pull: a server alters a tool's description after you approved it. With a remote server this needs no release on your side. Approving a server is not approving the text it will serve next week.
- **Shadowing**: In a second experiment, one server's tool description changed how the agent used a different, trusted server's email tool, so mail went to the attacker's address. The agent's own log did not say so. The cause is two servers in one context. Keep a server that touches sensitive data away from one you do not trust.
- **Output**: Claude Code's MCP documentation warns that servers which fetch external content can expose you to prompt injection. A tool that reads an issue, a web page or an email returns text someone else wrote, and the agent reads it as part of the task.
- **A cloned .mcp.json**: A repository can ship a `.mcp.json` that names a command to run. Claude Code asks before it starts those servers, so the prompt is the control. A prompt you accept without reading is not one. The MCP specification lists a malicious startup command in a client configuration as a local compromise path, with the example of a command that posts `~/.ssh/id_rsa` to a remote host.
- **Plaintext keys**: A token pasted into `env` or a header is stored as you wrote it, in `~/.claude.json` or in a `.mcp.json` that can be committed. Claude Code keeps its own sign-in in the macOS Keychain where available, but a key you typed into a server definition is a different thing, and the agent can read the file like any other. Invariant's demonstration went after the MCP config file for this reason.
- **Config changes**: Mitiga showed an npm `postinstall` script marking clone paths as trusted in `~/.claude.json`, after which a hook in a cloned repository re-pointed an OAuth MCP server at a local proxy and read each token. Anthropic ruled it out of scope, Mitiga reports, because the chain depends on consent the user had already given. The lesson is to know what is configured, which is the next section.

## Read what is configured

Servers come from several places, and reviewing a repository's `.mcp.json` does not show them all. Plugins can add servers, and so can claude.ai connectors.

| Scope | Loads in | Shared | Stored in |
| --- | --- | --- | --- |
| **Local** (default) | That project only | No | `~/.claude.json`, under the project's path |
| **Project** | That project only | Yes, through version control | `.mcp.json` in the project root |
| **User** | All your projects | No | `~/.claude.json` |

When one name is defined twice, the higher scope wins: local, then project, then user, then plugin servers, then claude.ai connectors, and field values are not merged. Local scope is `~/.claude.json`, not `.claude/settings.local.json`.

```
claude mcp list                  # every server and its status
claude mcp get <name>            # one server's definition
claude mcp remove <name> --scope user
claude mcp reset-project-choices # forget your .mcp.json approvals
/mcp                             # inside a session: status, sign-out, disable
```

- **Status**: `/mcp` shows `Connected`, `Needs authentication`, `Failed to connect`, `Pending approval` or `Disabled for this project` for each server. Its **Clear authentication** action revokes a remote sign-in, and **Disable** turns a server off for one project without deleting it.
- **Secrets in a file**: In `.mcp.json`, write `${VAR}` or `${VAR:-default}` instead of the value. Expansion works in `command`, `args`, `env`, `url` and `headers`. An unset variable with no default only produces a warning in `claude mcp list` and `/mcp`, and the server loads with the literal text.
- **Blocked variables**: In a remote server's `url` and `headers`, `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN` and `AWS_BEARER_TOKEN_BEDROCK` read as empty, so a repository's `.mcp.json` cannot send those to a server it names. Your other variables expand as written.
- **headersHelper**: A server can set a `headersHelper`, which Claude Code runs as an arbitrary shell command. For a project or local server it runs only after you accept the workspace trust dialog for that directory.

## Approve less, allow less

### Project servers from a clone

In an interactive session, Claude Code asks before it uses a server from a project's `.mcp.json`. Since v2.1.196, an approval committed to the repository's own `.claude/settings.json` (`enableAllProjectMcpServers` or `enabledMcpjsonServers`) is ignored until you trust the folder, so a clone cannot approve its own servers. Approvals in your `~/.claude/settings.json`, in managed settings and in `--settings` still apply, so keep `enableAllProjectMcpServers` out of your user settings unless you mean it.

- **No prompt**: **In `claude -p`, Agent SDK and cloud sessions there is no approval prompt, and project servers load without asking.** To keep one out, add it to `disabledMcpjsonServers`, which blocks it in every mode, or start with `--strict-mcp-config` or `--setting-sources user`.
- **Reject**: A `disabledMcpjsonServers` entry in any settings file rejects the server and shows it as `Rejected`.

### Rules for mcp__server__tool

MCP permission rules use the server name as you configured it, then optionally a tool name.

```
$ .claude/settings.json or ~/.claude/settings.json
{
  "permissions": {
    "allow": [
      "mcp__github__get_*"
    ],
    "deny": [
      "mcp__github__delete_repository",
      "mcp__filesystem"
    ]
  }
}
```

- **Forms**: `mcp__puppeteer` matches every tool from that server. `mcp__puppeteer__*` does the same. `mcp__puppeteer__puppeteer_navigate` matches one tool.
- **Globs**: An allow rule may use a glob only after a literal `mcp__<server>__` prefix, as in `mcp__github__get_*`. An unanchored allow such as `mcp__*` is skipped with a warning. Deny and ask rules accept `mcp__*`, and a tool matched by a bare-name deny is removed from the model's context.
- **No arguments**: A rule cannot match a tool's arguments. Claude Code skips any `mcp__` rule that has parentheses and lists it in the invalid-settings dialog and in `claude doctor`. Decide per tool, not per argument.
- **Order**: Prefer naming read-only tools in `allow` and leaving the rest to prompt. An allow on a whole server turns off the per-call check for every tool it has now and every tool it adds later.

Permissions cover what the agent does with MCP tools. They do not change what the server process can do on your machine. For that, the controls are the token it holds and where it runs.

## Limit what a server holds, and who can add one

- **Least privilege**: Give each server its own credential with the narrowest scope and the shortest life that works: read-only, one repository, one project. The MCP specification's guidance on scope minimization says broad tokens widen the damage from a leak and make revocation disruptive. [Token scopes for AI agents](https://ranwhat.com/guides/ai-agent-token-scopes) goes through the common providers.
- **Not the shell's**: Put the key in the server's own `env` through `${VAR}`, not in the shell you start `claude` from, where every Bash command inherits it. The [guide to blocking .env reads](https://ranwhat.com/guides/claude-code-env-secrets) covers keeping the agent out of the files.
- **Pin versions**: `npx -y some-server` fetches the newest release at each start, so whoever publishes the package chooses what runs with your access. Write the exact version, as in `npx -y some-server@1.4.2`, and update it on purpose. This pins code you run locally. It does nothing for a remote server, which can change on its own.
- **Remote URLs**: Use `https`. Look at a remote server's tool list again now and then, because nothing tells you when its descriptions change.
- **Isolation**: Claude Code's security page suggests running the whole process in the sandbox runtime or a dev container for extra isolation. Its Bash sandbox covers Bash commands, so do not assume it contains an MCP server you launched.

### For an organisation

By default anyone can connect any server. Managed settings change that.

- **managed-mcp.json**: Deployed to `/Library/Application Support/ClaudeCode/` on macOS, `/etc/claude-code/` on Linux and WSL, or `C:\Program Files\ClaudeCode\` on Windows. Users get only those servers and cannot add others. An empty `mcpServers` map turns MCP off. Any user on the machine can read the file, so keep keys out of its `env`.
- **Allow and deny**: `allowedMcpServers` and `deniedMcpServers` match by `serverUrl`, `serverCommand` or `serverName`. Setting `allowManagedMcpServersOnly` to true makes the managed allowlist the only one. Otherwise users can widen it in their own settings. The denylist always wins.
- **Not by name**: A `serverName` is the label a user typed, so anyone can call a server `github`. Anthropic's documentation says it is not a security control. Match on `serverUrl` or the exact `serverCommand`. The command match ignores `env`.

## See what this machine has configured

[ranwhat reach](https://ranwhat.com/commands) reads the configuration your agents keep and lists the MCP servers and credential files they can reach. It only reads, and it writes no settings.

```
$ uvx ranwhat reach
```

[Install](https://ranwhat.com/install)

- **Reads**: Servers in Claude Code's `~/.claude.json` (user scope and each project's local scope), each project's `.mcp.json` and `managed-mcp.json`, plus the files of Claude Desktop, Cursor, VS Code, Gemini CLI, Qwen Code, GitHub Copilot CLI, Windsurf and Codex.
- **Per server**: What it runs or connects to, and four things worth a look: a secret written inline in `env`, an argument, a header or the URL (shown the way `clean` shows one, never whole, and `${VAR}` references are left alone); a package fetched at its newest version on every start; a remote, plain `http` or local URL; and a `.mcp.json` whose servers start without asking because `enableAllProjectMcpServers` is on.
- **Files**: Credential files an agent could read, such as `.env`, private keys, `.npmrc` and `~/.aws/credentials`, when no Claude Code `Read` deny rule covers them. It prints paths, never contents, and ends with a `permissions.deny` block to paste.
- **Does not**: **It reads the configuration, not the server.** It does not inspect a server's code or its tool descriptions for poisoning, and it does not tell you whether a package is trustworthy. Read the server's source and its tool list yourself.

`ranwhat reach ~/code/app` checks one project, and `--json` prints it as data. To see what agents already did, including MCP calls, run `uvx ranwhat check`.

## A short routine

```
claude mcp list                 # do you recognise every server?
uvx ranwhat reach               # inline secrets, unpinned packages, auto-approved .mcp.json
/permissions                    # inside Claude Code: which mcp__ rules apply
```

Remove what you do not use, move keys to `${VAR}`, pin the packages, and replace any whole-server allow with named tools. For the record of what the agent actually called, read the [Claude Code audit log guide](https://ranwhat.com/guides/claude-code-audit-log). For the rest of the controls, the [AI coding agent security checklist](https://ranwhat.com/guides/ai-coding-agent-security), and the other [guides](https://ranwhat.com/guides) are listed here.

## Sources

- [Connect Claude Code to tools via MCPscopes, commands, .mcp.json expansion, project approvals, headersHelper, OAuth storage](https://code.claude.com/docs/en/mcp)
- [Configure permissionsmcp__server__tool rules, globs, parentheses skipped](https://code.claude.com/docs/en/permissions#mcp)
- [Claude Code securityMCP security, trust dialog, no audit of servers](https://code.claude.com/docs/en/security#mcp-security)
- [Control MCP server access for your organizationmanaged-mcp.json, allow and deny lists, serverName caveat](https://code.claude.com/docs/en/managed-mcp)
- [Settings referencethe enable, disable, allow and deny MCP keys and where they can be set](https://code.claude.com/docs/en/settings-reference)
- [MCP security best practiceslocal server compromise, startup commands, scope minimization](https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices)
- [Invariant Labs: tool poisoning attackshidden descriptions, shadowing, rug pulls, the Cursor demonstration](https://invariantlabs.ai/blog/mcp-security-notification-tool-poisoning-attacks)
- [Mitiga: stealing MCP tokens in Claude Codethe trust and re-pointed server chain, the disclosure](https://www.mitiga.io/blog/claude-code-mcp-token-theft-mitm)
- [ranwhat/reach.pywhat reach reads and reports](https://github.com/MatijaMiki/ranwhat/blob/main/ranwhat/reach.py)
- [ranwhat READMEthe reach command, its files and its limits](https://github.com/MatijaMiki/ranwhat/blob/main/README.md)
