How to protect MCP tools from prompt injection
Treat tool descriptions and returned content as untrusted inputs. Inspect them before the model sees them, detect changes to approved definitions, and enforce permissions before a tool executes. Test whether a poisoned result can make the agent misuse another tool.
By ZeroLeaks · Updated
Protect each boundary
The tool list, the call, and the result need different checks. Blocking a result after the server responds cannot reverse a side effect that already happened.
- Before exposing a tool
- Inspect its description and schemas for injected instructions. Keep a reviewed definition and require review when it changes. Authentication identifies the server; it does not make its text trustworthy.
- Before executing a call
- Validate the tool name and arguments. Enforce user scope, destination restrictions, and approval requirements in code. Keep tokens scoped to the operations the session needs.
- Before returning content to the model
- Inspect tool results, resources, and prompts. Preserve the distinction between external content and the user's instructions. Handle detector failures explicitly.
- Across servers and sessions
- An untrusted result from one server can influence a call on another. Apply policy across the tools in one conversation while keeping different users' policy state separate.
Understand tool poisoning
A poisoned description can tell an agent that a routine search first requires reading a credential or sending a document elsewhere. A later definition change can introduce that instruction after the original tool was reviewed. Microsoft describes both description poisoning and this subsequent change in its MCP security guidance.
The same pattern can arrive in a tool result. An issue body may ask the agent to forward private context to an external mailbox. The model sees a plausible instruction inside task data; the application must still decide whether the proposed outgoing action is authorized.
Wrap the client and declare the policy
Shield's MCP wrapper checks listed definitions, pins accepted definitions, and checks the content returned by callTool, readResource, and getPrompt. It can also enforce a deterministic tool policy before calling the server.
The example below uses the hosted detector and blocks detected injections. Install @zeroleaks/shield and @modelcontextprotocol/sdk, set ZEROLEAKS_API_KEY and MCP_SERVER_URL, and adapt the rule names to the tools your server actually exposes. Supply the server's required authentication through its transport configuration. Create a separate policy for each agent conversation.
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { createHostedDetector, createToolPolicy } from "@zeroleaks/shield";
import { shieldMcpClient } from "@zeroleaks/shield/mcp";
const serverUrl = process.env.MCP_SERVER_URL;
if (!serverUrl) throw new Error("Set MCP_SERVER_URL");
const detector = createHostedDetector();
const policy = createToolPolicy({
deny: ["delete_*"],
rules: {
get_issue: { labels: ["untrusted"] },
send_email: { labels: ["sink"] },
},
});
const client = shieldMcpClient(
new Client({ name: "review-agent", version: "1.0.0" }),
{ detect: detector.options(), onDetection: "block", policy }
);
await client.connect(
new StreamableHTTPClientTransport(new URL(serverUrl))
);
const { tools } = await client.listTools();
// Give this checked list to your agent and route calls through client.
// Close client when the connection is no longer needed.Test the permission independently of detection
A detector can miss an instruction. You can still enforce a rule that a session which read an untrusted issue cannot send email. This local example exercises that rule without connecting to an MCP server or sending a message. Run it with Bun after installing @zeroleaks/shield.
The expected decision is untrusted_to_sink. The test only demonstrates the declared policy. It does not establish that every tool in your application is correctly labeled or that calls cannot bypass the wrapper. Test those integration paths separately.
import { strict as assert } from "node:assert";
import { createToolPolicy } from "@zeroleaks/shield";
const policy = createToolPolicy({
rules: { send_email: { labels: ["sink"] } },
});
const call = {
name: "send_email",
arguments: { to: "canary@example.invalid", body: "Synthetic test" },
};
assert.equal(policy.check(call).allowed, true);
policy.recordUntrusted("issue body");
const decision = policy.check(call);
assert.equal(decision.allowed, false);
assert.equal(decision.reason, "untrusted_to_sink");Know what the wrapper does not cover
Tool-result detection happens after the server executes the call. Enforce write permissions and approvals before that point. The wrapper does not scan every MCP method, raw request, notification, image, or audio payload. Review its documented coverage against your application.
Definition pins last for the wrapped client's lifetime unless you persist them. When a server announces a changed tool list, refresh it through the wrapped client. The MCP SDK's automatic refresh can use the unwrapped client, so follow the integration guide's refresh configuration.
Use scoped server credentials, validate authorization at the server, and restrict network access independently. Then red-team the connected agent, including cross-server calls and memory. Detection and permissions reduce different parts of the attack surface.
Questions
Email us if yours isn't here.
Does MCP authentication prevent prompt injection?
Authentication controls which server or user can connect. An authenticated server can still return malicious or compromised content. Validate authorization separately and treat tool descriptions and results as untrusted model inputs.
Does blocking a tool result stop the tool from running?
No. A result check happens after the call has run on the server. Use a pre-execution policy, server authorization, and approval controls to prevent unauthorized side effects.
Can I share one policy across every user?
Create a separate policy per conversation or session. The policy remembers untrusted reads and call counts. Share it across the tools used by that conversation, not across unrelated users.
Can Shield guarantee that an MCP agent will not be compromised?
No. Detection can miss attacks, policy coverage depends on the tools and labels you declare, and some MCP methods are outside the wrapper. Test your complete integration and retain least-privilege credentials and execution controls.
Related testing
- How to Red Team AI Agents with Tools and MemoryA deployment checklist for testing AI agents with tools, memory, and credentials: define boundaries, exercise real tool paths, inspect evidence, and rerun after fixes.
- Prompt Injection Detectors for RAG and Tool ResultsChoose a prompt injection detector using attack recall, false positives, input coverage, and end-to-end latency. Includes the published Shield comparison and its limits.
- Prompt Injection Testing for AI SystemsTest AI applications for prompt injection, instruction hijacking, tool abuse, and multi-turn attacks before they reach production.
- AI Red Teaming for LLM Apps and AgentsAutomated AI red teaming for LLM applications, agents, system prompts, retrieval flows, and tool-calling workflows.
- AI Agent Security TestingTest AI agents for prompt injection, unsafe tool calls, over-permissioned actions, secret exposure, and workflow boundary failures.
- LLM Security Scanner for AI AppsRun automated LLM security scans for prompt injection, prompt leakage, tool abuse, model behavior regressions, and AI security reports.