Engineering
MCP Policy Enforcement: How to Secure Model Context Protocol Tool Calls
The takeaway
Put a policy-enforcement proxy between an MCP client and its servers so each Model Context Protocol tool call is checked before it runs: allow low-risk calls, block disallowed ones, and hold consequential calls for a decision.
The idea, illustrated
Check the call before it reaches the tool.
Put policy in the tool-call path
- 01
MCP client
An agent requests a tool with specific arguments.
tools/call + arguments
- 02
Policy boundary
Evaluate the call. Allow, deny or require a decision.
Check before forwarding
- 03
MCP server
The tool receives calls permitted through the boundary.
Execute the permitted call
Only calls routed through the configured enforcement boundary are covered.
In this article
To enforce policy on MCP tool calls, put a policy-enforcement proxy between the MCP client and its servers so each tool invocation is checked against policy before it runs: low-risk calls are allowed, disallowed calls are blocked, and consequential calls are held for a decision. Below is how to set it up and check each outcome.
Definition: what MCP policy enforcement is
MCP policy enforcement is the practice of checking each Model Context Protocol tool call against an explicit policy before it executes. A policy-enforcement proxy sits on the MCP transport between the client (the application the agent runs in) and the MCP servers, inspects each tool invocation, and decides whether to allow, block, or hold it. It turns “the agent can call any tool the server exposes” into “this specific call was checked against policy, and here is the decision.”
Transport authentication is not tool-call policy
The MCP specification's authorization section decides whether a client may reach a server at all. For HTTP-based transports it uses OAuth 2.1: the MCP server acts as a resource server and validates the client's access token. For the stdio transport, the specification says implementations should not follow that flow and should take credentials from the environment instead. Authorization is optional in both cases.
Neither answers whether one specific tools/call should run right now. A valid token, or a server process the client started with its own credentials, can still call every tool the server exposes. Tool-call policy is a separate check on each request: which tool, and whether to allow it, block it, or hold it before the server runs it. You usually want both: authentication for who can connect, and policy for what a connected agent may do. Source: MCP specification, Authorization (revision 2026-07-28) (opens in a new tab).
Why MCP tool calls are a security boundary
MCP is an open protocol that connects AI agents and clients to external tools and data via servers. Those tool calls read and write real systems (files, databases, repos, APIs) so an MCP tool call is a security boundary, not a passive read. Without a policy layer in front of it, three risks compound:
- Over-broad tool access. An agent is typically granted every tool a connected server exposes, with no per-call check on what it actually does.
- Prompt-injection-driven misuse. Untrusted content pulled into context can steer the agent into invoking tools it should not, exfiltrating data or making destructive changes.
- Unaudited invocations. Tool calls fire with no durable record of what ran, against what, and whether anyone authorized it.
A policy-enforcement proxy narrows all three for the calls routed through it: each call is checked, disallowed calls are blocked, consequential calls are held, and each decision is logged.
Unguarded MCP tool calls vs. policy-enforced MCP
| Property | Unguarded MCP tool calls | Policy-enforced MCP |
|---|---|---|
| Each routed tool call checked against policy | No | Yes, before the server runs it |
| Blocks disallowed calls | No, server decides | Yes, the call never reaches the server |
| Holds consequential calls for a decision | No | Yes, a held call does not run without a decision |
| Leaves a record of each decision | No durable record | Yes, a decision log (MCP Guard 0.1.0 writes it locally, unsigned) |
| Works without rewriting the agent | N/A | Yes, the proxy wraps the server command |
| Limits what injected instructions can run | No | Yes, for routed calls: blocked tools never reach the server |
Set it up with MCP Guard
These steps use MCP Guard, Permission Protocol's open-source stdio proxy. Every command and output below was run against @permission-protocol/mcp-guard 0.1.0 on October 14, 2026. You need Node.js; npx fetches the pinned package.
- Write the policy. Each rule matches an exact tool name and sets
allow,block, orrequire_approval.default_actionapplies when no rule matches; here, unlisted tools are blocked. - Try it against a test server. Save the test server and run the piped command in the same folder as
pp.config.yaml. It sends three tool calls through the proxy. - Wrap your own server, observing first. Observe mode forwards every call and logs what would have been held or blocked. Switch to enforcement once the decisions look right.
- Point your MCP client at the proxy. The client launches MCP Guard, and MCP Guard starts your server. Use absolute paths, because the client may start the proxy from a different working directory.
default_action: blockmode: enforcerules: - id: allow-read-file tool: read_file action: allow - id: hold-production-deploy tool: deploy_production action: require_approval - id: block-user-data-delete tool: delete_user_data action: blockExpected output. Held and blocked replies come straight from the proxy, so they can print before the allowed call's result; match responses by id.
{"jsonrpc":"2.0","id":2,"error":{"code":-32002,"message":"Held for approval: Matched rule \"hold-production-deploy\""}}
{"jsonrpc":"2.0","id":3,"error":{"code":-32001,"message":"Blocked: Matched rule \"block-user-data-delete\""}}
{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"ran read_file"}]}}# Log each decision but forward every call (observe mode):npx -y @permission-protocol/[email protected] --config pp.config.yaml --mode observe -- node my-mcp-server.js# After reviewing the decisions, enforce the rules:npx -y @permission-protocol/[email protected] --config pp.config.yaml --mode enforce -- node my-mcp-server.jsThe client config uses the Claude Desktop format; confirm the format in your client's documentation. The 0.1.0 package README on npm lists an older package name in its install line; use the commands on this page.
Review the MCP Guard setup and its boundaries or see @permission-protocol/mcp-guard 0.1.0 on npm (opens in a new tab). The GitHub repository's main branch includes unreleased changes, so the npm release is what these steps describe.
Reading the results
| Outcome | What the client receives | Why |
|---|---|---|
| Allowed | The server's normal result | A rule (or default_action: allow) matched, so the call was forwarded unchanged. |
| Held | Error -32002, "Held for approval: ..." | A require_approval rule matched. Without --approval-port, the call returns this error and does not reach the server. |
| Blocked | Error -32001, "Blocked: ..." | A block rule matched, or no rule matched and default_action is block. |
- Error -32002 with the message “Denied by administrator” means a person denied a held call in the local approval UI.
- If the config file is missing or invalid, the proxy exits before it starts your server.
- An unrecognized
--modevalue is ignored, and the mode in the config file applies. - Each decision is written to stderr.
pp-receipts.jsonlis written to the proxy's working directory only when that directory is writable, and a failed write is silent. The stderr lines carry the label “receipt:”; they are unsigned local decision records.
What MCP Guard 0.1.0 does not do
- It matches exact tool names. It does not inspect arguments, so one rule cannot allow a write to one path and block another.
- It does not sign its decision log or record who approved a held call. A log written by the proxy on the same machine is a self-report, not independent evidence.
- It covers only calls routed through it. Tools the agent runs directly, other servers, and credentials used outside the proxy are not covered.
- It checks single
tools/callrequests only. Batched JSON-RPC messages, other MCP methods such asresources/read, and lines that are not JSON pass through unchecked. - It works with the stdio transport only.
- Held calls do not expire. The optional local approval UI (the
--approval-portflag) has no sign-in, listens on all network interfaces, and accepts requests from any web page. Anyone who can reach the port, or any page open in a browser on that machine, can read held calls and approve them, and a late approval still runs the tool. Use it only to evaluate test tools.
If your reviewers need a signed record of which person approved a specific call, that is a different kind of evidence. See what an authorization receipt proves.
Common mistakes
- Trusting MCP servers by default. A connected server is not automatically safe. Treat its tools as a boundary, not a trusted extension of the agent.
- Granting broad tool scopes. Handing the agent every tool a server exposes maximizes blast radius. Scope to what the task needs.
- No gate on destructive tools. Deletes, writes, and external sends should be held for a decision, not auto-run.
- No audit trail of tool invocations. If you cannot show what was called and who authorized it, you have no answer when an auditor asks.
- Enforcing only in the app. App-level checks can be bypassed by another client or path. Enforce at the protocol layer so routed calls are covered.
Frequently asked questions
What is MCP policy enforcement?
MCP policy enforcement is checking each Model Context Protocol tool call against an explicit policy before it executes. A policy-enforcement proxy sits between the MCP client and its servers, inspects each tool invocation, and decides whether to allow a low-risk call, block a disallowed one, or hold a consequential call for a decision, logging each decision.
Why do MCP tool calls need a security layer?
MCP tool calls read and write real systems, so they are a security boundary. Without a policy layer, agents can be granted over-broad tool access, prompt injection can steer them into misusing tools, and invocations go unaudited. A policy-enforcement layer turns each routed MCP tool call into a checked, logged decision instead of an implicit trust grant.
How do you enforce policy on MCP without rewriting the agent?
Insert a proxy at the stdio layer between the MCP client and its servers. The client launches the proxy instead of the server, and the proxy starts the server, so neither needs code changes. The proxy checks each tool call against policy and allows, blocks, or holds it before it reaches the server.
Last updated: October 14, 2026. Permission Protocol is the external authorization layer for AI agent actions, per-action human approval and tamper-evident receipts. See the OWASP Agentic Top 10 mapping for the full control coverage.
Put it into practice
Explore the MCP integration
See how a tool-call gate fits between your agent and its MCP server.