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, params }
        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_KEY—Agent 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