DocumentationAgent Action Firewall

MCP Governance Proxy

The @aaf/mcp-proxy package provides a governance layer for Model Context Protocol (MCP) tool calls. It intercepts tools/call JSON-RPC messages, evaluates them against AAF policies, and blocks or allows tool usage based on your rules.

Architecture

@aaf/mcp-proxy sits between the MCP client (your AI agent or Claude Desktop) and the MCP server process. It operates at the JSON-RPC message level: every tools/call message is intercepted, evaluated against AAF policies, and either forwarded to the server or blocked — before the server ever sees it. All other MCP messages (initialize, tools/list, notifications, etc.) pass through unchanged.

Evaluation pipeline

When a tools/call message arrives the proxy runs two sequential checks:

  1. Local governance pre-check — Instantly evaluated in-process using config-based rules: governance profile (unrestricted, read_only, locked), server-level tool allowlist/denylist, and per-agent tool allowlist/denylist. No network call required. Short-circuits to deny if a local rule matches.
  2. AAF policy evaluation — If the local check passes, the proxy submits the tool name and arguments to POST /v1/actions on the AAF API. The three-tier AAF cascade (OPA/Rego → DB policy rules → heuristic fallback) returns allow, deny, or require_approval.

For require_approval decisions the proxy polls GET /v1/approvals/:id every 2 seconds (configurable) until a human approves or denies, or the timeout elapses (default 30 seconds). On timeout or denial the proxy returns a JSON-RPC error to the client.

If the AAF API is unreachable the proxy defaults to fail closed (deny all) — configurable to fail open via failClosedOnError: false.

Sequence diagram

sequenceDiagram
    participant Agent as AI Agent / MCP Client
    participant Proxy as @aaf/mcp-proxy
    participant AAF as AAF /v1/actions
    participant Approver as Human Approver
    participant Server as MCP Server

    Agent->>Proxy: tools/call { name, arguments }

    Note over Proxy: Local governance pre-check<br/>(profile, allowlist, denylist)

    alt Local deny
        Proxy-->>Agent: JSON-RPC error -32000 (blocked)
    else Local pass
        Proxy->>AAF: POST /v1/actions { tool, operation, parameters }
        AAF-->>Proxy: decision: allow | deny | require_approval

        alt allow
            Proxy->>Server: tools/call (forwarded)
            Server-->>Proxy: tools/call result
            Proxy-->>Agent: tools/call result
        else deny
            Proxy-->>Agent: JSON-RPC error -32000 (blocked by policy)
        else require_approval
            loop Poll every 2s (up to approvalTimeoutMs)
                Proxy->>AAF: GET /v1/approvals/:id
                AAF-->>Proxy: status: pending | approved | denied
            end
            alt approved
                Proxy->>Server: tools/call (forwarded)
                Server-->>Proxy: tools/call result
                Proxy-->>Agent: tools/call result
            else denied / timeout
                Proxy-->>Agent: JSON-RPC error -32001 (approval timeout/denied)
            end
        end
    end

Transport modes

The proxy supports two transport modes that differ only in how messages are delivered — the policy evaluation pipeline is identical in both cases.

Stdio transport — The proxy spawns the MCP server as a child process (stdio: pipe). Client stdin is read line-by-line (newline-delimited JSON-RPC). tools/call lines are intercepted; all other lines are forwarded directly to the server's stdin. Server stdout is piped directly back to the client. This is the mode used by the CLI and proxy.wrapStdio().

HTTP/SSE transport — The proxy runs as an Express/Fastify middleware on your POST /mcp route. Non-tools/call requests fall through via next(). Blocked tool calls are returned as SSE error events (text/event-stream) before the request reaches your MCP server handler. This is the mode used by proxy.createSSEMiddleware().

Environment variables

VariableDefaultDescription
AAF_API_KEYAgent API key (alternative to --api-key flag)
MCP_PROXY_REQUEST_TIMEOUT_MS5000Timeout for individual AAF API HTTP requests (policy evaluation call). Distinct from approvalTimeoutMs which governs the human approval polling window.
AAF_API_URLhttps://api.agentactionfirewall.comAAF API base URL (self-hosted deployments)

Installation

npm install @aaf/mcp-proxy

Quick Start: CLI

Wrap any MCP stdio server with governance in a single command:

npx @aaf/mcp-proxy \
  --api-key YOUR_AAF_API_KEY \
  -- uvx mcp-server-filesystem /tmp

This starts the MCP filesystem server as a child process, with all tools/call messages routed through AAF for policy evaluation before reaching the server.

CLI Options

FlagDescriptionDefault
--api-keyAAF API key (or AAF_API_KEY env var)Required
--api-urlAAF API endpointhttps://api.agentactionfirewall.com
--agent-idAgent identifiermcp-proxy
--verboseEnable debug loggingfalse

Programmatic Usage

Stdio Transport

Wrap a child process MCP server:

import { MCPProxy } from '@aaf/mcp-proxy';

const proxy = new MCPProxy({
  apiKey: process.env.AAF_API_KEY!,
  apiUrl: 'https://api.agentactionfirewall.com',
  agentId: 'my-mcp-proxy',
});

// Wrap any stdio-based MCP server
proxy.wrapStdio('uvx', ['mcp-server-filesystem', '/tmp']);

HTTP/SSE Middleware

Use as Express/Fastify middleware for HTTP-based MCP servers:

import express from 'express';
import { MCPProxy } from '@aaf/mcp-proxy';

const app = express();
const proxy = new MCPProxy({
  apiKey: process.env.AAF_API_KEY!,
});

// Intercept MCP requests before your handler
app.post('/mcp', proxy.createSSEMiddleware(), yourMCPHandler);

Programmatic Interception

Check individual tool calls without wrapping a server:

const proxy = new MCPProxy({
  apiKey: process.env.AAF_API_KEY!,
});

const result = await proxy.interceptToolCall('file_write', {
  path: '/etc/passwd',
  content: 'malicious content',
});

if (!result.allowed) {
  console.error(`Blocked: ${result.reason}`);
  // result.reason: "SSRF: access to system files denied"
}

Policy Examples

Block Dangerous File Operations

package aaf.policy

# Deny writes to system directories
decision = "deny" {
  input.action.tool == "file_write"
  path := input.action.params.path
  startswith(path, "/etc/")
}

decision = "deny" {
  input.action.tool == "file_write"
  path := input.action.params.path
  startswith(path, "/usr/")
}

Require Approval for Shell Commands

package aaf.policy

# All shell commands require human approval
decision = "require_approval" {
  input.action.tool == "run_command"
}

Allow Only Specific MCP Tools

package aaf.policy

# Allowlist specific tools
allowed_tools := {"file_read", "web_search", "calculator"}

decision = "deny" {
  not input.action.tool in allowed_tools
}

Supported MCP Servers

The proxy works with any MCP-compliant server. Commonly used with:

ServerPackageDescription
Filesystemmcp-server-filesystemFile read/write operations
GitHubmcp-server-githubRepository operations
PostgreSQLmcp-server-postgresDatabase queries
Brave Searchmcp-server-brave-searchWeb search
Puppeteermcp-server-puppeteerBrowser automation

Monitoring

All intercepted tool calls appear in the AAF dashboard:

  • Action feed: Each MCP tool call shows as an action with tool name, parameters, and decision
  • Audit trail: Full hash-chained audit of every tool call, approval, and result
  • Analytics: Tool usage patterns, denial rates, and latency metrics

Best Practices

Start in monitor mode. Use AAF's dry-run mode initially to observe tool call patterns before enforcing policies.

Use tool allowlists. Rather than blocking specific dangerous tools, define an allowlist of tools your agent should use.

Combine with guardrails. MCP tool parameters are scanned by the guardrails engine for prompt injection before policy evaluation.

Next Steps