MCP Action Firewall
# π₯ MCP Action Firewall
[](https://python.org)
[](LICENSE)
[](https://modelcontextprotocol.io)
### Works with any MCP-compatible agent
[](https://claude.ai)
[](https://cursor.sh)
[](https://codeium.com/windsurf)
[](https://openai.com)
[](https://gemini.google.com)
[](https://github.com/openclaw)
A transparent **MCP proxy** that intercepts dangerous tool calls and requires **OTP-based human approval** before execution. Acts as a circuit breaker between your AI agent and any MCP server.
## How It Works
```
ββββββββββββ stdin/stdout ββββββββββββββββββββ stdin/stdout ββββββββββββββββββββ
β AI Agent β ββββββββββββββββββΊ β MCP Action β ββββββββββββββββββΊ β Target MCP Serverβ
β (Claude) β β Firewall β β (e.g. Stripe) β
ββββββββββββ ββββββββββββββββββββ ββββββββββββββββββββ
β
Policy Engine
βββββββββββββββββ
β Allow? Block? β
β Generate OTP β
βββββββββββββββββ
```
MCP servers don't run like web servers β there's no background process on a port. Instead, your AI agent (Claude, Cursor, etc.) **spawns the MCP server as a subprocess** and talks to it over stdin/stdout. When the chat ends, the process dies.
The firewall inserts itself into that chain:
```
Without firewall:
Claude ββspawnsβββΊ mcp-server-stripe
With firewall:
Claude ββspawnsβββΊ mcp-action-firewall ββspawnsβββΊ mcp-server-stripe
```
So you just **replace the server command** in your MCP client config with the firewall, and tell the firewall what the original command was:
**Before** (direct):
```json
{ "command": "uvx", "args": ["mcp-server-stripe", "--api-key", "sk_test_..."] }
```
**After** (wrapped with firewall):
```json
{ "command": "uv", "args": ["run", "mcp-action-firewall", "--target", "mcp-server-stripe --api-key sk_test_..."] }
```
Then the firewall applies your security policy:
1. β
**Safe calls** (e.g. `get_balance`) β forwarded immediately
2. π **Dangerous calls** (e.g. `delete_user`) β blocked, OTP generated
3. π Agent asks user for the code β user replies β agent calls `firewall_confirm` β original action executes
## Installation
```bash
pip install mcp-action-firewall
# or
uvx mcp-action-firewall --help
```
## Quick Start β MCP Client Configuration
Add the firewall as a wrapper around any MCP server in your client config:
```json
{
"mcpServers": {
"stripe": {
"command": "uv",
"args": ["run", "mcp-action-firewall", "--target", "mcp-server-stripe --api-key sk_test_abc123"]
}
}
}
```
That's it. Everything after `--target` is the **full shell command** to launch the real MCP server β including its own flags like `--api-key`. The firewall doesn't touch those args, it just spawns the target and sits in front of it.
### More Examples
<details>
<summary>Claude Desktop with per-server rules</summary>
```json
{
"mcpServers": {
"stripe": {
"command": "uv",
"args": [
"run", "mcp-action-firewall",
"--target", "uvx mcp-server-stripe --api-key sk_test_...",
"--name", "stripe"
]
},
"database": {
"command": "uv",
"args": [
"run", "mcp-action-firewall",
"--target", "uvx mcp-server-postgres --connection-string postgresql://...",
"--name", "database",
"--config", "/path/to/my/firewall_config.json"
]
}
}
}
```
</details>
<details>
<summary>Cursor / Other MCP Clients</summary>
```json
{
"mcpServers": {
"github": {
"command": "uvx",
"args": [
"mcp-action-firewall",
"--target", "npx @modelcontextprotocol/server-github"
]
}
}
}
```
</details>
## The OTP Flow
When the agent tries to call a blocked tool, the firewall returns a structured response:
```json
{
"status": "PAUSED_FOR_APPROVAL",
"message": "β οΈ The action 'delete_user' is HIGH RISK and has been locked by the Action Firewall.",
"action": {
"tool": "delete_user",
"arguments": { "id": 42 }
},
"instruction": "To unlock this action, you MUST ask the user for authorization.\n\n1. Show the user the following and ask for approval:\n Tool: **delete_user**\n Arguments:\n{\"id\": 42}\n\n2. Tell the user: 'Please reply with approval code: **9942**' to allow this action, or say no to cancel.\n3. STOP and wait for their reply.\n4. When they reply with '9942', call the 'firewall_confirm' tool with that code.\n5. If they say no or give a different code, do NOT retry."
}
```
> **Argument visibility guarantee:** The arguments shown to the user are frozen at interception time β they are taken from the original blocked call, not from what the agent passes to `firewall_confirm`. The agent cannot change the arguments after the OTP is issued.
The `firewall_confirm` tool is automatically injected into the server's tool list:
```json
{
"name": "firewall_confirm",
"description": "Call this tool ONLY when the user provides the correct 4-digit approval code to confirm a paused action.",
"inputSchema": {
"type": "object",
"properties": {
"otp": {
"type": "string",
"description": "The 4-digit code provided by the user."
}
},
"required": ["otp"]
}
}
```
## Configuration
The firewall ships with sensible defaults. Override with `--config`:
```json
{
"global": {
"allow_prefixes": ["get_", "list_", "read_", "fetch_"],
"block_keywords": ["delete", "update", "create", "pay", "send", "transfer", "drop", "remove", "refund"],
"default_action": "block",
"otp_attempt_count": 1
},
"servers": {
"stripe": {
"allow_prefixes": [],
"block_keywords": ["refund", "charge"],
"default_action": "block"
},
"database": {
"allow_prefixes": ["select_"],
"block_keywords": ["drop", "truncate", "alter"],
"default_action": "block"
}
}
}
```
**Rule evaluation order:**
1. Tool name starts with an allow prefix β **ALLOW**
2. Tool name contains a block keyword β **BLOCK** (OTP required)
3. No match β fallback to `default_action`
**`otp_attempt_count`** β maximum number of failed OTP attempts before the pending action is permanently locked out. Defaults to `1` (any wrong code cancels the request). Increase for more forgiving UX, keep at `1` for maximum security.
**Per-server rules** extend (not replace) the global rules. Use `--name stripe` to activate server-specific overrides.
## CLI Reference
### `--target` (required)
The full command to launch the real MCP server. This is the server you want to protect:
```bash
mcp-action-firewall --target "mcp-server-stripe --api-key sk_test_abc123"
mcp-action-firewall --target "npx @modelcontextprotocol/server-github"
mcp-action-firewall --target "uvx mcp-server-postgres --connection-string postgresql://localhost/mydb"
```
### `--name` (optional)
Activates per-server rules from your config. Without it, only global rules apply:
```bash
mcp-action-firewall --target "mcp-server-stripe" --name stripe
```
### `--config` (optional)
Custom config file path. Without it, uses `firewall_config.json` in your current directory, or the bundled defaults:
```bash
mcp-action-firewall --target "mcp-server-stripe" --config /path/to/my_rules.json
```
### `-v` / `--verbose` (optional)
Turns on debug logging (written to stderr, won't interfere with MCP traffic):
```bash
mcp-action-firewall --target "mcp-server-stripe" -v
```
## Project Structure
```
src/mcp_action_firewall/
βββ __init__.py # Package version
βββ __main__.py # python -m support
βββ server.py # CLI entry point
βββ proxy.py # JSON-RPC stdio proxy
βββ policy.py # Allow/block rule engine
βββ state.py # OTP store with TTL
βββ default_config.json # Bundled default rules
```
## Try It β Interactive Demo
See the firewall in action without any setup:
```bash
git clone https://github.com/starskrime/mcp-action-firewall.git
cd mcp-action-firewall
uv sync
uv run python demo.py
```
The demo simulates an AI agent and walks you through the full OTP flow:
1. β
**Safe call** (`get_balance`) β passes through instantly
2. π **Dangerous call** (`delete_user`) β blocked, OTP generated
3. π **You enter the code** β action executes after approval
## Known Limitations
### Argument Inspection
The firewall matches on **tool names only**, not argument values. This means a tool like `get_data({"sql": "DROP TABLE users"})` would pass if `get_` is in your allow list, because the policy engine only sees `get_data`.
**Workaround:** Use explicit tool names in your allow/block lists and set `"default_action": "block"` so unrecognized tools require approval.
> π§ **Roadmap:** Argument-level inspection (scanning argument values against `block_keywords`) is planned for a future release.
## Development
```bash
# Install dev dependencies
uv sync
# Run tests
uv run pytest tests/ -v
# Run the firewall locally
uv run mcp-action-firewall --target "your-server-command" -v
```
## License
MIT
TDQS
Scored across 14 tools
Most tools have clearly distinct purposes, but some get-* tools like get-annotated-message and get-structured-content could be confused, and get-resource-links vs get-resource-reference are similar.
Tool names mix hyphens (get-env) and underscores (firewall_confirm), and verbs vary inconsistently (echo, get-, toggle-, simulate-, trigger-, gzip-), lacking a uniform pattern.
14 tools is a reasonable count, but the server name implies a firewall/pause/confirm focus, while most tools are demo utilities, creating a scope mismatch.
The server name suggests a firewall use case, but only one tool (firewall_confirm) supports that; the remaining tools are unrelated demos, leaving a significant gap between promise and functionality.