border-patrol-mcp
by pavlealeksic
README.md
# border-patrol-mcp
```
_______________
| BORDER |
| PATROL |
|_______________|
___
(o_o) "HALT! State your business."
<| |\
| |
_| |_
(_) (_)
```
**A policy-enforcing MCP gateway.** Your agent connects to *one* MCP server — border-patrol — and every tool call to every real MCP server behind it gets stopped at the checkpoint and asked the important questions: **allow**, **deny**, or *"papers, please"* (a human has to approve it).
The agent only ever talks to the guard. The dangerous servers are only reachable through the checkpoint. There is no road around it. We checked.
```
Agent (Claude, Cursor, …)
│ ← sees ONE MCP server: border-patrol
▼
border-patrol ── policy engine: allow / deny / ask ── audit log (JSONL)
│ │
├──► postgres MCP └── approval CLI (you: `border-patrol approve req_42`)
├──► filesystem MCP
└──► shell MCP
```
## Why does this exist
Because one day your agent will decide the users table is "legacy". Because "drop the test database" and "drop the database" are the same sentence to a language model at 2am. Because client-side permission prompts are inconsistent, easy to click through, and written by optimists.
Border-patrol puts enforcement **at the protocol layer**, in one place you control, with an audit trail of everything the agent did — and, more importantly, everything it *tried* to do.
## Install & run
```bash
npm install -g border-patrol-mcp
```
(From source instead: `git clone`, `npm install`, `npm run build`, and swap `border-patrol` for `node /path/to/dist/cli.js` below.)
Create `border-patrol.yaml`:
```yaml
servers:
postgres:
command: npx
args: ["-y", "@modelcontextprotocol/server-postgres", "postgres://localhost/mydb"]
filesystem:
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/you/project"]
rules:
# Hard-block DDL. The users table stays. Forever.
- tool: "postgres__query"
match:
args.query: { regex: "(?i)^\\s*(drop|truncate|alter|grant)\\b" }
action: deny
reason: "DDL is forbidden through this gateway"
# Writes need a human to say yes. Once. Per exact call.
- tool: "postgres__query"
match:
args.query: { regex: "(?i)^\\s*(delete|update)\\b" }
action: ask
# No catch-all needed: unmatched calls fall back to `defaults`
# (onUnknown: allow), while tools annotated destructiveHint still
# route to `ask`. An explicit `- tool: "*", action: allow` would
# override that annotation safety net — write one only if you mean it.
```
Point your MCP client at border-patrol instead of the real servers:
```json
{
"mcpServers": {
"border-patrol": {
"command": "npx",
"args": ["-y", "border-patrol-mcp", "serve", "--config", "/path/to/border-patrol.yaml"]
}
}
}
```
Works with any MCP client — it's just a standard stdio MCP server with strong opinions.
## How the checkpoint works
Every tool from every downstream server is re-exposed as `<server>__<tool>` (e.g. `postgres__query`). For each call, the first matching rule wins:
- **`allow`** — forwarded downstream verbatim. Have a nice day.
- **`deny`** — blocked before it reaches the server. The agent gets an error explaining why, and a lecture it didn't ask for.
- **`ask`** — not executed. The officer appears, the agent gets an approval id, and tells you. You run:
```bash
border-patrol list # see what's waiting at the checkpoint
border-patrol approve req_42 # stamp it. or: deny req_42
```
The agent then retries the **exact same call** and it goes through — **once**. Approvals are single-use and bound to the exact arguments, so an approval for `DELETE FROM users WHERE id = 5` does not magically also cover `WHERE 1=1`. Nice try.
When no rule matches, tool **annotations** from the MCP spec take over: a tool marked `destructiveHint: true` defaults to `ask` (configurable via `defaults.onDestructiveHint`). You get sane protection even for servers you've never written rules for.
### Matchers
Rules match on the namespaced tool name (glob: `postgres__*`) and on argument values via dot-paths:
```yaml
match:
args.query: { regex: "(?i)^delete\\b" } # "(?i)" prefix = case-insensitive
args.path: { contains: "/etc/" }
args.env: { equals: "production" }
args.input: { notRegex: "(?i)secret" }
```
All conditions in a rule must match. String arguments are whitespace-normalized before matching (trimmed, runs collapsed), because "D R O P TABLE" is not as clever as the agent thinks it is.
### Fail-closed by design
The officer assumes guilt:
- Invalid config or rule regex → refuses to start.
- Downstream server unreachable → its tools simply don't exist. Nothing bypasses the checkpoint.
- Approval timeout → treated as denied.
- Bug in the guard itself → the call is blocked, not forwarded.
### Audit log
Every call is appended to `.border-patrol/audit.jsonl` (path configurable): tool, arguments, decision, reason, outcome, latency. Grep it, tail it, or frame the entry where the agent tried to `DROP TABLE users` and you caught it.
## CLI
```
border-patrol serve [--config path] run the gateway (what agents connect to)
border-patrol approve <id> stamp the papers
border-patrol deny <id> turn the call around
border-patrol list [--status pending] see who's waiting at the checkpoint
```
Config resolution: `--config` flag → `$BORDER_PATROL_CONFIG` → `./border-patrol.yaml`.
## Development
```bash
npm run build # compile to dist/
npm test # policy engine unit tests
npm run test:e2e # full gateway e2e (allow/deny/approve/retry/audit)
```
## Honest limitations
- **The guard is a tripwire, not a vault door.** Regex matching can be defeated by determined obfuscation. Pair it with least-privilege credentials downstream (read-only DB roles, sandboxed shells) for real safety. Officer Pat Rollo is vigilant, but he cannot read base64.
- **Enforcement only works if border-patrol is the only path.** Don't also connect the raw servers directly to the agent. That's a hole in the fence.
- Resources and prompts are not proxied yet (tools only).
- Approval currently happens out-of-band via the CLI; MCP elicitation support is planned for clients that support it.
## Roadmap
- MCP elicitation for inline approval prompts (clients that support it)
- Streamable-HTTP transport so border-patrol can run as a shared team checkpoint
- Rate limits / per-session budgets for destructive calls
- SQL-aware matching via a real parser (so the officer *can* read base64)
- Small web dashboard for approvals and audit browsing
## License
MIT. The checkpoint is open source. The checkpoint never closes.
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues