Skip to main content
Glama
pavlealeksic

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&nbsp;&nbsp;&nbsp;&nbsp;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.