Est.

MCP Server Security Best Practices for Tool Execution

LLMs now choose which tools to call at runtime, forcing security teams to rethink every layer.

Correspondent · · 12 min read
Cover illustration for “MCP Server Security Best Practices for Tool Execution”
Running AI-Generated Code Safely · August 25, 2026 · 12 min read · 2,738 words

MCP servers break a security assumption most engineering teams have never had to question: the decision about which tool to call, and what data to hand it, now happens at runtime, made by a model instead of a person doing a code review. That single shift changes what "secure" means at every layer downstream. Authentication, input handling, privilege scoping, sandboxing, and a logging gap that keeps showing up only after the damage is already done: all of it gets reshaped by the fact that nobody's reviewing the call before it happens.

In traditional software, a developer writes the line that calls the payments API, and that line sits still until someone reviews and merges a change to it. You can draw the attack surface on a whiteboard, hand it to a pentester, get a straight answer back. MCP doesn't work that way. An agent reads a tool description at runtime and decides on its own whether and how to call it, so the attack surface moves with every new tool registered and every session that agent happens to run. Firewalls and WAFs were built to defend known request paths. MCP tool calls get generated on the fly from natural language the model treats as instruction rather than something worth double-checking, and none of that older tooling maps onto the problem cleanly.

A tool description is just text. A human reading "this tool fetches weather data" might still go check the code before trusting it, out of habit, or paranoia, or just because that's the job. An LLM reading that same string doesn't have the reflex. It reads the string as an instruction on how to behave, and whatever's embedded in that description becomes something the model might actually act on.

Knostic's research, reported by Dark Reading in 2025, found a large share of MCP servers already sitting on the open web with no authentication or access control at all. The CVE count came in fast too: more than a handful of distinct vulnerabilities disclosed within roughly two months of adoption picking up. That pace tells you the protocol's security model wasn't hardened for production when it shipped. Patching and network hardening still matter, but neither one touches this particular gap. Closing it means putting controls at the exact point where the agent calls the tool, which is the layer-by-layer walk that follows.

Venn diagram: Traditional Software vs MCP Security. Compares Traditional Software and MCP Servers; overlap: Shared Security Needs.

The four attack classes that actually show up in MCP production environments

Tool poisoning goes first because it's the most direct exploit of the trust problem above. A malicious or compromised MCP server writes adversarial instructions into a tool's description field, and the model has no reliable way to tell that text apart from a legitimate one. Invariant Labs' 2025 disclosure showed how far this goes: a poisoned description told an agent to quietly exfiltrate file contents by smuggling them out as a tool argument, while the tool's visible output looked completely normal to anyone watching the session. Research on open-source MCP servers in 2025 found a meaningful chunk of them already carrying signs of this kind of poisoning. This isn't an exotic edge case; it's routine enough now to sit in the standard threat model next to SQL injection and XSS.

Rug pulls work on a timing gap instead of a trust gap. An attacker stands up a server that looks legitimate, gets it approved through whatever review process an enterprise runs, then changes the tool's behavior after approval clears. Most change-control processes check a server once, at registration, and never again, so the server just keeps operating under trust it earned before it turned into something else.

The CVEs aren't theoretical exercises for a whitepaper. CVE-2025-6514, found in mcp-remote, took a server-supplied authorization_endpoint URL and passed it straight into the operating system's open() call with no sanitization. Just connecting to a malicious server was enough to trigger arbitrary command execution, no authentication bypass required, and the package had already racked up serious download numbers before anyone caught it. Anthropic's own official Git server wasn't spared either: CVE-2025-68143, CVE-2025-68144, and CVE-2025-68145 chained three flaws together, letting a crafted repository path read files outside the repo, inject arguments, and eventually reach full remote code execution. Line the two cases up and the root cause is identical both times: tool arguments or server metadata get fed straight into shell construction (child_process.exec, subprocess.run, os.system), with nobody validating them first.

The fourth class is quieter, and more structural than the other three. It's the confused deputy problem, and it shows up whenever an MCP proxy server acts on a user's behalf using credentials broader than that user should hold. Picture a server fronting a third-party API with its own service account attached. An attacker who can manipulate that server's behavior inherits whatever the service account can do, well beyond what the individual caller was ever supposed to touch. Any MCP server sitting on credentials wider than what its callers actually need is a confused deputy waiting for the right nudge.

One more thing, since it trips people up constantly: even the tools engineers use to build and debug MCP servers turn out to be exploitable. CVE-2025-49596, an RCE in MCP Inspector, and CVE-2026-23744, arbitrary server installation in MCPJam Inspector, make the same point from two different angles. Don't expose MCP Inspector, or anything like it, on a network-accessible port.

Authentication requirements that the current MCP spec actually mandates

Diagram: MCP Auth Spec: Three Rewrites in Under a Year. Visualizes: Show a timeline of the three MCP authentication specification revisions and their key mandates.

The MCP auth spec has been rewritten three times in under a year, and each rewrite followed something in the previous version getting exploited in the wild. March 2025 standardized on OAuth 2.1. June 2025 formally classified MCP servers as OAuth Resource Servers, split the authorization function into its own dedicated authorization server, and made Protected Resource Metadata a requirement instead of a suggestion. November 2025 added Client ID Metadata Documents and made PKCE mandatory for every public client, no exceptions carved out anywhere in the text.

That June revision carries an architectural mandate that's easy to miss if you're skimming for compliance checkboxes instead of reading closely. The authorization server handles user login, issues tokens, registers clients. The MCP server, sitting as the resource server, only validates tokens and enforces access rules; authenticating users falls outside its job description entirely. Collapse those two roles into one server and you've misimplemented the current spec, no matter how secure the result feels from the inside.

PKCE earns its own paragraph here. Most MCP clients (CLI tools, IDE extensions, desktop apps) have nowhere safe to keep a client secret. PKCE protects the authorization code flow against interception when a client secret isn't an option, and that describes most of this ecosystem most of the time. Running OAuth 2.1 without it is a known, documented weak spot, and the current spec closed that door on purpose.

Token binding gets underrated more than it should. Resource Indicators, per RFC 8707, tie a token to a specific MCP server. Skip that binding, and a token issued for one server can get replayed against a different one, a lateral movement path that binding shuts cleanly. Also worth a look: the Enhanced Tool Definition Interface, ETDI, a proposal that binds tool definitions to signed JWTs and represents tool permissions as OAuth 2.0 scopes, speaks directly to the rug pull problem above: a signed definition that changes after approval becomes detectable instead of invisible.

Before deploying anything over HTTP, check the implementation against the post-November 2025 spec specifically. Anything built to the March 2025 version has known gaps, and those gaps are exactly what the later revisions exist to close.

Input validation and shell-injection prevention at the tool boundary

Any value arriving at a tool, whether it came from a user, an LLM, or another agent in a multi-agent chain, is untrusted until proven otherwise. Prompt injection can manipulate an LLM into handing a tool malformed or outright adversarial arguments, and the tool schema alone won't stop that. A schema describes the shape data is supposed to take. Enforcing that shape at runtime takes something more, since nothing checks it automatically. Schemas are documentation, not defense, and a lot of teams treat them as both.

Shell construction is the single riskiest pattern showing up in MCP server code today, and it belongs on the mandatory audit list before anything ships. Search the codebase for child_process.exec, subprocess.run, os.system. Each one is a candidate injection point. The mcp-server-git CVE chain from earlier is the textbook case of what happens when repository paths and arguments flow into shell commands unsanitized. Where you can, reach for a language-native library instead of shelling out; isomorphic-git instead of spawning a git subprocess directly is the obvious swap.

URL and metadata fields need the same scrutiny. CVE-2025-6514 showed exactly what happens when a server-supplied field like authorization_endpoint gets used without validation or escaping before a system call. Treat any value a remote server hands over during the protocol handshake as though it might be hostile, because sometimes it is.

At runtime, tool arguments need to pass a strict schema check before execution: reject fields that weren't expected, enforce type and range limits, don't let raw input travel downstream unchecked. For tools accepting file paths, validate against an allowlist of permitted directories and reject path traversal patterns, including the encoded variants that try to sneak ../ past a naive filter.

Output deserves the same suspicion as input, and teams tend to forget this entirely. A tool's response, once it lands back in the model's context, can carry embedded instructions of its own: prompt injection arriving through the response path instead of the request path. Sanitize what comes back, especially from third-party or user-supplied tool sources, before it re-enters the conversation the model is reasoning over.

Validation shrinks the blast radius of a bad call, but it leaves untouched what the tool is actually capable of doing once it runs. That gap is a privilege problem, not a validation one.

Privilege scoping so a compromised tool call can't become a full breach

The confused deputy problem isn't just a threat category to file away. It's a design flaw waiting to be triggered, and fixing it has to be architectural, not a policy memo nobody reads twice. A server should request only the permissions its specific tool operations actually need, never blanket access to whatever API it happens to front.

Scoping starts at the token level. Use OAuth 2.0 scopes to represent individual tool permissions instead of one broad grant covering an entire service. ETDI's approach, encoding permissions into signed JWTs tied to each tool definition, is the clean version of this idea done right. a token scoped to read:calendar should never work for write:email, even when both tools live behind the same server process.

But scoping only works if it gets checked at the moment a tool actually runs, not just when it's first registered. Teams most commonly skip permission enforcement at invocation time, usually because checking once at setup is easier and everyone quietly assumes it holds forever. That assumption doesn't hold on its own, so enforce the check every single time, not just the first time.

Allowlisting tools is a smaller lift with a bigger payoff than it gets credit for. List explicitly which tools a given agent or client may call, and reject anything outside that list even if the server technically exposes more. This blunts tool poisoning directly, since a poisoned description can't redirect an agent toward a tool it was never permitted to touch in the first place.

For anything with real consequences (writing to a production system, sending an external message, or rotating a credential), put a human in the loop before execution runs. That's a privilege control too, maybe the one that matters most once everything upstream has already failed. Credentials, meanwhile, should never sit inside tool definitions or server code. Keep them in a secrets manager, rotate them on a schedule, audit who's touching them.

Sandboxed execution environments for the code tools actually run

Diagram: Sandbox Isolation Tiers for MCP Tool Execution. Visualizes: Show a ranked or tiered diagram of three sandbox isolation levels matched to their appropriate MCP workload.

Standard containers were never built for this threat model. Docker and runc share the host kernel across every container running on a machine, so a kernel exploit inside one container can reach the host and every neighbor sitting beside it. MCP tool execution frequently means running code an AI generated, code no human wrote or reviewed line by line before it ran. Treating that code as trustworthy enough for a standard container isn't a minor oversight you patch in the next sprint. LLM-generated code introduces new bugs in a meaningful share of cases, even on tasks that were supposedly just fixing an existing bug. The safer default assumes agent-generated code might be hostile until proven otherwise, and builds from there.

Different threat models call for different isolation tiers, and picking the wrong one is easy to do without noticing until something goes wrong. Production agents running untrusted or AI-generated code belong in Firecracker microVMs or Kata Containers, where the isolation boundary is hardware-enforced and a compromised guest can't touch the host kernel or any sandbox sitting next to it. Compute-heavy agents that don't need much I/O can often get away with gVisor, which gives strong isolation at less overhead than a full microVM. Trusted internal automation, where a person has actually reviewed the code, can run in hardened containers with seccomp profiles, AppArmor, and Linux capabilities stripped down. That tier, though, is only acceptable once the code has genuinely been audited, not assumed safe by default.

A few things matter more than others when picking a sandbox platform for this job. Provisioning speed sits near the top: agents call tools dynamically at runtime, and multi-second cold starts break the interaction loop the model depends on, so sub-second, ideally sub-100-millisecond, provisioning is the bar worth clearing. Stateful execution matters almost as much, since a sandbox that can be snapshotted and resumed lets a long agent workflow pause and pick back up without re-running every prior step from scratch. Network egress controls deserve attention too, because unrestricted outbound calls are one of the more common paths attackers use in tool-poisoning attacks to get data out, and an allowlist of approved endpoints closes that door outright. For enterprise and regulated workloads, running sandboxes inside your own cloud account rather than a vendor's keeps execution data under your control, and coverage like SOC 2, HIPAA, and GDPR needs to be there from day one, not sold as an add-on later.

Daytona builds around this exact set of requirements. Its sandboxes provision in under 90 milliseconds, support stateful workflows through snapshots that capture the OS, installed packages, dependencies, and configuration so an agent resumes from a fully prepared environment instead of starting cold, and run on customer-managed compute with SOC 2, HIPAA, and GDPR coverage built in rather than upsold later. It was designed around agent workloads from the start, not retrofitted from a general-purpose container platform after the fact, which is why it's worth naming here specifically.

The MCP spec itself points in this direction too, calling for platform-appropriate sandboxing and explicit privilege grants for any capability beyond the basics. Treat sandbox escape as a design constraint from day one, not something you patch after deployment once it has already gone sideways.

Logging, monitoring, and the tooling gap that OWASP flags most often

MCP08 in the OWASP MCP Top 10 (comprehensive logging) is the control teams skip most often, and it happens to be the one that matters most the moment something actually breaks. When an incident happens, logs are the only reconstruction path left. Without them, the postmortem is just guesswork with extra steps.

Generic application logging misses things that matter specifically here. Which tool got invoked, by which client or agent identity, with what arguments (sanitized, since credential values should never land in a log file), and what came back from the tool: that's the baseline, not the ceiling. Just as important, and often skipped entirely, is recording which version of a tool's definition was active at the moment it got called. That single detail is the audit trail that lets a team catch a rug pull after the fact, by showing exactly when a tool's behavior changed and what it looked like before it did.

Authentication, input validation, privilege scoping, sandboxing: every layer above assumes something eventually slips through anyway, because something always does. Logging turns that assumption into something a team can actually act on, rather than a gap they discover only after it is too late.

Sources

  1. labs.cloudsecurityalliance.org
  2. github.com
  3. akto.io
  4. modelcontextprotocol.io
  5. truefoundry.com
  6. descope.com
  7. reco.ai
  8. obot.ai

More in Running AI-Generated Code Safely