A transparent MCP proxy that intercepts dangerous tool calls and requires OTP-based user approval.
Project description
๐ฅ MCP Action Firewall
Works with any MCP-compatible agent
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):
{ "command": "uvx", "args": ["mcp-server-stripe", "--api-key", "sk_test_..."] }
After (wrapped with firewall):
{ "command": "uv", "args": ["run", "mcp-action-firewall", "--target", "mcp-server-stripe --api-key sk_test_..."] }
Then the firewall applies your security policy:
- โ
Safe calls (e.g.
get_balance) โ forwarded immediately - ๐ Dangerous calls (e.g.
delete_user) โ blocked, OTP generated - ๐ Agent asks user for the code โ user replies โ agent calls
firewall_confirmโ original action executes
Installation
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:
{
"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
Claude Desktop with per-server rules
{
"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"
]
}
}
}
Cursor / Other MCP Clients
{
"mcpServers": {
"github": {
"command": "uvx",
"args": [
"mcp-action-firewall",
"--target", "npx @modelcontextprotocol/server-github"
]
}
}
}
The OTP Flow
When the agent tries to call a blocked tool, the firewall returns a structured response:
{
"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:
{
"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:
{
"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:
- Tool name starts with an allow prefix โ ALLOW
- Tool name contains a block keyword โ BLOCK (OTP required)
- 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:
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:
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:
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):
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:
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:
- โ
Safe call (
get_balance) โ passes through instantly - ๐ Dangerous call (
delete_user) โ blocked, OTP generated - ๐ 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
# 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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mcp_action_firewall-0.3.0.tar.gz.
File metadata
- Download URL: mcp_action_firewall-0.3.0.tar.gz
- Upload date:
- Size: 22.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.2 {"installer":{"name":"uv","version":"0.10.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
22645b4b63bb0259273a2f2b930e658aa380693f385db2390b6513eca6874272
|
|
| MD5 |
94bc70275c52c3f1f2cf4d88ea211c7f
|
|
| BLAKE2b-256 |
a3e27155142ca04e1d0d90ec575a3be06a62771ee83c964bc29ff23561033605
|
File details
Details for the file mcp_action_firewall-0.3.0-py3-none-any.whl.
File metadata
- Download URL: mcp_action_firewall-0.3.0-py3-none-any.whl
- Upload date:
- Size: 18.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.2 {"installer":{"name":"uv","version":"0.10.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2e812ae6f7f07ef0090e66b2dd1341515a350435fdf7675084a8ee7688766050
|
|
| MD5 |
6bebd9d6219777190087283581636172
|
|
| BLAKE2b-256 |
5cf3b2a62953e34bbffeb1be96bcb9f8b7604af63b723e694292fef61b774fa5
|