Part of the AI coding agent security checklist

Securing MCP servers for coding agents

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

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

01 / Reach

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.

02 / Risks

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.
03 / Config

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.

ScopeLoads inSharedStored in
Local (default)That project onlyNo~/.claude.json, under the project's path
ProjectThat project onlyYes, through version control.mcp.json in the project root
UserAll your projectsNo~/.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.
04 / Scope

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.

05 / Limit

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 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 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 [email protected], 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.
06 / Check

See what this machine has configured

ranwhat reach 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
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.

07 / Next

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. For the rest of the controls, the AI coding agent security checklist, and the other guides are listed here.