PERMISSION/PROTOCOL
Back to blog

Engineering · April 2, 2026

Securing MCP Tool Calls with Approval Gates and Signed Receipts

What MCP Does (and Doesn't Do)

The Model Context Protocol (MCP), introduced by Anthropic, gives AI agents a standardized way to call external tools. An MCP server exposes a set of tools (database queries, file operations, API calls, code execution) and the agent invokes them by name with structured parameters.

This is genuinely useful. Standardized tool interfaces mean agents can work across environments without bespoke integration code. An agent running in Claude can call the same tools as one running in a custom LangChain pipeline.

What MCP doesn't provide: any notion of human authorization before a tool executes. The protocol handles the how of tool invocation. It says nothing about the whether, whether a human intended this invocation, whether it was explicitly approved, or whether there's any record that a specific human said "yes, run this tool, right now, with these parameters."

The gap: MCP tool calls execute with the agent's full capabilities the moment the model decides to invoke them. There is no native approval gate between "agent decides to act" and "tool executes."

The Attack Surface MCP Creates

Consider a typical MCP server configuration:

{
  "tools": [
    { "name": "execute_sql", "description": "Run a SQL query on the production database" },
    { "name": "deploy_service", "description": "Deploy a Docker image to the Kubernetes cluster" },
    { "name": "send_email", "description": "Send an email to any address" },
    { "name": "modify_env_vars", "description": "Update environment variables for a service" }
  ]
}

Each of these tools is individually reasonable to expose. But without an approval gate, an AI agent can invoke any of them at any time, for any reason it determines is valid. The agent's context window is the only constraint.

Prompt injection changes this. If an attacker can insert text into the agent's context (through a document it reads, a web page it fetches, or malicious content in a database row), the injected instruction can trigger a tool call. The agent, following the injected instruction, calls deploy_service or modify_env_vars as instructed. MCP executes it. No human was involved.

Introducing mcp-guard

@permission-protocol/mcp-guard is a middleware layer that sits between your agent and your MCP tool server. It intercepts tool call invocations before they reach the tool server, routes them for human approval if the tool is gated, and only passes the call through once a signed receipt has been issued.

Install it in your Node.js MCP server:

npm install @permission-protocol/mcp-guard

Wrap your existing MCP server:

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { withMcpGuard } from '@permission-protocol/mcp-guard';

const server = new McpServer({ name: 'my-tools', version: '1.0.0' });

// Define which tools require human approval before execution
const guardedServer = withMcpGuard(server, {
  apiKey: process.env.PP_API_KEY,
  requireApproval: [
    'deploy_service',
    'modify_env_vars',
    'execute_sql',   // flag destructive SQL as gated
  ],
  notifyChannel: 'slack:#deploys',
  receiptExpiry: '30m',
});

That's the entire integration. The MCP server now has an approval gate. Tool calls to ungated tools (like read-only queries) pass through immediately. Calls to gated tools pause, route for human approval, and only execute once a signed receipt exists.

What Happens When a Gated Tool Is Called

Here's the flow in detail:

  1. Agent decides to call deploy_service with image: "myapp:v2.1.0", env: "production"
  2. mcp-guard intercepts the call before it reaches the tool server
  3. Guard creates an authorization request, records the exact tool name and parameters
  4. Guard sends a notification to the configured channel (Slack, email, webhook) with a link to approve or deny
  5. Guard returns a pending response to the agent, the call is not executed yet
  6. A human reviews the request: "Agent wants to deploy myapp:v2.1.0 to production. Approve?"
  7. Human approves → Permission Protocol issues a signed receipt
  8. Guard releases the tool call, passes it through to the MCP server with the receipt attached
  9. Tool executes. Receipt is stored as proof of authorization.

If the human denies, the tool call never executes. The agent receives a denial response and must re-plan. If no one responds within the receipt expiry window, the request times out and the agent must re-request.

The Signed Receipt

Every approved tool call produces a receipt like this:

{
  "id": "rcpt_mcp_7f3a9b2c",
  "tool": "deploy_service",
  "parameters": {
    "image": "myapp:v2.1.0",
    "env": "production"
  },
  "authorizedBy": "[email protected]",
  "issuedAt": "2026-04-02T14:30:00Z",
  "expiresAt": "2026-04-02T15:00:00Z",
  "signature": "ed25519:a9f3c2...",  // Ed25519 over canonical JSON of above
  "publicKey": "pp_pk_8a7b..."
}

The signature is an Ed25519 signature over the canonical JSON of the receipt body. Anyone with the public key can verify the receipt offline, without a network call. The tool name and parameters are inside the signed payload: the receipt is specific to this exact invocation, not just the tool class.

This matters for audits. You're not proving "a human approved deploy_service calls." You're proving "Sarah approved deploying myapp:v2.1.0 to production at 14:30 on April 2." The scope is the specific action, not the capability.

Configuring Risk Tiers

Not every tool call needs human review. mcp-guard supports tiered configuration:

const guardedServer = withMcpGuard(server, {
  apiKey: process.env.PP_API_KEY,
  rules: [
    // Always block, requires human approval
    { tools: ['deploy_service', 'modify_env_vars'], policy: 'require-approval' },

    // Auto-approve but always log with receipt
    { tools: ['execute_sql'], policy: 'auto-approve-and-receipt',
      condition: { queryType: 'SELECT' } },

    // Block destructive SQL
    { tools: ['execute_sql'], policy: 'require-approval',
      condition: { queryType: ['INSERT', 'UPDATE', 'DELETE', 'DROP'] } },

    // Everything else passes through unguarded
    { tools: '*', policy: 'passthrough' },
  ],
});

This lets you be surgical. Read operations proceed with automatic receipts (logged, verifiable, but no approval friction). Write operations route for human review. Destructive or production-touching operations always require explicit approval.

Why This Matters Beyond MCP

The MCP approval gate pattern applies to any tool interface that gives AI agents access to real-world side effects. Whether your agent is using MCP, a custom function-calling interface, or a LangChain tool, the structural requirement is the same: an external authority must issue a signed receipt before the action executes, and the execution layer must verify that receipt.

MCP makes the interception point easy to identify, every tool call flows through a defined interface. That's actually an advantage. A well-defined tool boundary is where you want your approval gate, because it's the only point where you can consistently intercept all consequential actions before they happen.

See Permission Protocol for the full authorization model, and visit the quickstart to start with one gated tool call and one signed receipt.

Gate your MCP tool calls. Ship with a receipt every time.

Get Started →