Skip to content

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.

TypeScript · MCP client setup
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.

TypeScript · local policy regression
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.

Put this into practice