Dev Tool Experiences
All articles

· 8 min read

MCP Security in 2026: Treat Tool Poisoning and Privilege Creep as One Problem

By K. Farahani

  • tools

Yes: developers using MCP servers should worry about tool poisoning now, but the practical fix isn’t trying to teach the model to ignore malicious prose. Make an untrusted tool’s output unable to reach credentials, filesystem reads, repository writes, or network egress it does not need—and stop handing every new server the same broad identity.

The failure mode is a trust-boundary bug, not weird prompt text

Tool poisoning can arrive in a tool description, parameter schema, or result. The mundane version is a server advertising get_release_status, returning a plausible release report, and slipping in an instruction to inspect a local config file or send a value to an external URL. The agent already has the tools, the file access, and perhaps a GitHub token. The malicious server only needs to steer it toward using them.

That’s why “the agent asks before it acts” is useful but insufficient. A confirmation that says “Run deploy check?” is not meaningful if the actual parameters include a repo outside the task, a hidden destination, or a request for secrets. The user must see the target, the operation, and the data boundary—not merely a friendly tool name.

MCP’s own security policy is unusually blunt about the baseline: a client trusts the servers it connects to, and a local stdio server runs with the client’s privileges. That is intentional protocol behavior, not an exploit the protocol will save you from. Treat adding an MCP server as installing executable software plus granting it a place in an agent’s decision loop.

Split tools by blast radius before you add another server

The common bad setup is one coding agent with a broad personal access token, access to the whole checkout, shell execution, browser access, a cloud CLI, and a few MCP servers from public registries. It works right up to the point that a ticket body, documentation page, or tool result supplies an instruction the model follows. At that point, every capability in the shared session is part of the attack surface.

Use separate agent contexts for separate trust levels. An external documentation or issue-tracker MCP server can live in a read-only research context. Your repository-write server belongs in a coding context. Production observability, cloud administration, payroll, and customer-data tools should be separate again, with their own approval rules. Do not put a “quick helper” server and a production deploy tool in the same agent session just because the UI permits it.

This is the part people call privilege creep, although it often happens in a single afternoon: the agent starts with repo search, then gets shell access for tests, then an issue tracker, then a GitHub token to open PRs, then a cloud credential because it needs logs. Each grant sounds reasonable. The combination means a poisoned result can turn an otherwise low-impact tool call into a cross-system action.

Give each server its own identity, not your all-purpose token

Do not put a general-purpose PAT or cloud credential in every MCP server’s environment. Create a separate credential for each server, with the narrowest scopes and short lifetime your provider supports. A docs-search server does not need repository write. A PR-opening server does not need organization administration. A metrics server does not need deployment permission.

For remote MCP services using OAuth, check the implementation rather than assuming OAuth makes this safe. The current MCP authorization requirements say clients must request a token for the target resource and servers must validate that the token was issued for them; they also prohibit token passthrough to an upstream API. If your server accepts a bearer token and forwards it unchanged downstream, it has erased the audience boundary that was supposed to contain the credential.

Make the policy visible in configuration. For example, an agent allowed to change code should be constrained to one repository and a short list of operations: read files under the checkout, run the test command, create a branch, and open a PR. “Can execute shell commands” is not a policy. git status, npm test, and git diff are a starting allowlist; curl, arbitrary bash -c, credential stores, and ~/.ssh are separate decisions.

Sandbox local servers as though their package had a bug—because it might

A local MCP server is a program your client launches, often through a command in a configuration file. Review the exact command and arguments in the same pull request that adds the server. If the server needs only the repository, mount only that directory. If it has no reason to call the network, deny the network. If it only reads files, make the filesystem mount read-only.

For a local indexer or formatter that does not need network access, this is a useful Docker baseline to adapt to your approved image:

docker run --rm \
  --read-only \
  --cap-drop=ALL \
  --security-opt=no-new-privileges \
  --network=none \
  -v "$PWD:/workspace:ro" \
  "$MCP_SERVER_IMAGE"

This will break plenty of servers. That is the point: add back one mount, capability, or egress rule only when you can name the feature that needs it. Docker is not a complete sandbox, and it won’t help a remote MCP server with an over-scoped OAuth token, but it turns “the server can see my laptop” into a choice you have to make explicitly.

Pin what you can, then detect what you cannot pin

A server that looked harmless at install time can change its tool definitions or its behavior later. Keep MCP configuration in version control, pin package versions or container digests, and require review for new commands, environment variables, mounted paths, server URLs, and scopes. A quick first pass for a JSON config file is enough to catch many accidental additions:

git diff -- .mcp.json
jq -r '.mcpServers | to_entries[] | [.key, .value.command, ((.value.args // []) | join(" "))] | @tsv' .mcp.json

Adapt the path and JSON key to your client. The important habit is reviewing the launched command, not only the server’s display name. A pinned local package does not solve a remote server changing its responses, either. Log tool names, arguments, caller identity, approval decisions, result size, and outbound destinations. Those records make it possible to answer the question that matters after an incident: which untrusted input caused which privileged action?

Use schemas as guardrails, not as a prompt-injection cure

Structured input and output reduce ambiguity. Define narrow JSON schemas, reject undeclared fields, constrain strings such as repository names and paths, and return typed data instead of free-form “instructions.” A get_build_status tool should return a status, build ID, timestamp, and log URL—not a blob of prose that another agent can interpret as operational guidance.

But a valid schema cannot prove that a value is safe to act on. A result can validly contain an attacker-controlled issue title, URL, filename, or ticket comment. Validate at the execution boundary: the repository-write service verifies the repository; the filesystem service enforces allowed roots; the HTTP client applies an egress allowlist; the deployment service checks environment and change approval. If the model is tricked, those backend checks should still refuse the action.

A 30-minute audit worth doing this week

  1. Export the MCP configuration used by your editor and CLI. Mark every server as trusted local, trusted remote, or external/untrusted; disable anything you cannot place.
  2. For each enabled server, write down its process identity, credentials, filesystem paths, network destinations, and destructive operations. If you cannot describe one of these, remove the permission until you can.
  3. Find shared tokens. Replace them with per-server credentials and reduce scopes. For remote OAuth servers, verify audience validation and that the server is not passing client tokens through to upstream APIs.
  4. Move external-content servers—web search, tickets, documentation, email, public GitHub—out of sessions that can deploy, read secrets, or write broadly across repositories.
  5. Turn on approval for data export, deletion, payments, deployments, and permission changes. Make the prompt show exact arguments and destination, then test that the backend rejects out-of-policy calls even without a human click.

Tool poisoning is bad because the model can be persuaded. Privilege creep is what makes persuasion expensive. Fix the second problem first: a poisoned tool result that can only influence a read-only research agent is annoying; the same result inside an agent holding a cloud-admin token is an incident waiting for a natural-language trigger.

Sources & citations

  1. [1]Model Context Protocol — Security Best Practices
  2. [2]Model Context Protocol — Authorization Security Considerations
  3. [3]Model Context Protocol — Security Policy and Trust Model
  4. [4]OWASP MCP Security Cheat Sheet
  5. [5]OWASP — MCP Tool Poisoning