# Runtime authorization and audit for any MCP server in 2 minutes

> Originally published on the Vulnify blog: https://www.vulnify.io/blog/mcp-server-firewall-runtime-authorization-audit

Plug an MCP server into an agent and the agent can call every tool that server exposes. Reading tickets or searching docs is fine. Exporting a contact list, deleting rows, sending email or creating a payment is a different story, and nothing in the protocol stops a call like that once the model decides to make it.

A firewall for MCP servers fills that gap. It sits on the tool call, before the side effect, and answers one question: should this specific call run?

That's what [Vulnify](https://www.vulnify.io/?utm_source=hashnode&utm_medium=referral&utm_campaign=mcp-server-firewall) does. Each tool call becomes an action (`READ_DATA`, `WRITE_DATA`, `DELETE_DATA`, `EXPORT_DATA`, `SEND_EMAIL`) with a destination and a record count. Vulnify answers **ALLOW**, **REVIEW** or **BLOCK**, with a 0-100 risk score and the reasons. REVIEW waits for a person to approve in the Vulnify app or in Slack. Every decision lands in the audit log.

Below are three real setups, depending on where your MCP calls happen. Pick one.

## Before you start

- A Vulnify account. There's a free plan, no card.
- An API key from the app (`vln_test_...` keeps events in the sandbox while you try things, `vln_live_...` for real use).
- Node.js 18 or newer for the Claude Code hook.

## Setup 1: Claude Code, every MCP tool call checked (hook)

If you just connect an MCP server to Claude Code, Claude decides when to ask for permission. A `PreToolUse` hook changes that: it runs before each matching tool call, asks Vulnify first, and Claude Code obeys the answer. That's enforcement, not guidance.

**1. Install the hook** from your project folder:

```bash
npx @vulnify/cli hook claude-code
```

It copies the hook (one Node file, no dependencies) to `.claude/hooks/vulnify-hook.mjs` and adds a `PreToolUse` entry to `.claude/settings.json`. If `VULNIFY_API_KEY` is already exported, it uses that and saves nothing. If not, it asks for the key with hidden input and saves it to `.claude/settings.local.json` (file mode 600, added to `.gitignore`). Add `--user` to install for every project in `~/.claude`, `--yes` to skip prompts (needs `VULNIFY_API_KEY` exported), or `--uninstall` to remove it.

**2. Restart Claude Code** and run `/hooks` to check it loaded.

**Manual install (fallback).** Download the file and register it yourself:

```bash
mkdir -p .claude/hooks
curl -fsSL https://raw.githubusercontent.com/vulnify/vulnify-examples/main/claude-code-hook/vulnify-hook.mjs \
  -o .claude/hooks/vulnify-hook.mjs
export VULNIFY_API_KEY=vln_test_...   # don't commit it
```

Then add this to `.claude/settings.json` (or `~/.claude/settings.json` for every project):

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|PowerShell|Write|Edit|MultiEdit|NotebookEdit|WebFetch|mcp__.*",
        "hooks": [
          {
            "type": "command",
            "command": "node",
            "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/vulnify-hook.mjs"],
            "timeout": 30,
            "onFailure": "block"
          }
        ]
      }
    ]
  }
}
```

The `mcp__.*` part of the matcher is the firewall: every tool from every MCP server you've added to Claude Code goes through Vulnify. `onFailure: "block"` needs Claude Code 2.1.295 or later.

**What happens to each call:**

| Vulnify says | Claude Code does |
| --- | --- |
| ALLOW | Normal permission flow |
| REVIEW | Denied with "waiting for approval in Vulnify" and the event id. Approve in the app or Slack, then ask Claude to retry |
| BLOCK | Denied, with Vulnify's reasons |
| Vulnify unreachable, bad key, timeout | Denied (fails closed), unless you opt into fail-open |

MCP tools are judged by name. Send and post tools become `SEND_EMAIL` (for mail servers) or `EXPORT_DATA`. Delete tools become `DELETE_DATA`. Export, share, upload, transfer and payment tools become `EXPORT_DATA`. Create and update tools become `WRITE_DATA`. Read tools are skipped unless you set `VULNIFY_HOOK_CHECK_READS=true`. The hook also covers shell commands: `rm` or `DROP TABLE` become `DELETE_DATA`, and `curl -X POST`, `scp` or `git push` become `EXPORT_DATA` to an external API.

Full reference: [Enforce in Claude Code (hook)](https://docs.vulnify.io/mcp/claude-code-hook/?utm_source=hashnode&utm_medium=referral&utm_campaign=mcp-server-firewall).

## Setup 2: your own agent calling other MCP servers (gateway)

If your agent runs in production and calls third-party MCP servers, put the decision in that path. Send the tool call to the MCP gateway first. It maps the tool name to an action, decides, records the event, and tells you whether the call is allowed:

```bash
curl -X POST https://api.vulnify.io/v1/gateway/mcp \
  -H "Authorization: Bearer $VULNIFY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "routeId": "ROUTE_ID",
    "tool": "crm.export_contacts",
    "resource": "Customer Database",
    "destination": "EXTERNAL_EMAIL",
    "recordsAffected": 800,
    "arguments": { "limit": 800 }
  }'
```

The response is a normal decision plus a `gateway` object with `mappedAction` and `allowed`. Run the downstream tool only when `finalDecision` is `ALLOW`. Vulnify never executes `arguments`; your upstream MCP server does, after an allowing decision. The same check is available as the `check_mcp_tool_call` tool when you run the Vulnify MCP server locally with an API key (`npx -y @vulnify/mcp`).

Reference: [MCP gateway](https://docs.vulnify.io/api/gateway-mcp/?utm_source=hashnode&utm_medium=referral&utm_campaign=mcp-server-firewall).

## Setup 3: your own MCP server (in-process adapter)

If you run the MCP server yourself, guard the handlers inside it. The Node.js SDK's `guardMcpHandler` adapter wraps a tool handler and asks Vulnify before the handler body runs. Use it when the server runs in your process; use the gateway when a caller sends tool calls to Vulnify. See [SDK adapters](https://docs.vulnify.io/sdks/adapters/?utm_source=hashnode&utm_medium=referral&utm_campaign=mcp-server-firewall).

## What a held call looks like

Take an agent asked to export 800 records to an outside email address. Run that request in the [playground on vulnify.io](https://www.vulnify.io/?utm_source=hashnode&utm_medium=referral&utm_campaign=mcp-server-firewall#playground) (action `EXPORT`, 800 records, external email destination, data not tagged as customer PII) and the risk engine answers:

```json
{
  "decision": "REVIEW",
  "riskLevel": "HIGH",
  "riskScore": 65,
  "reasons": ["Export operation", "External destination", "Large data volume (800 records)"]
}
```

The tool doesn't run. A reviewer gets the alert in Slack (Approve and Deny buttons with the Slack app) or in the Vulnify app. The decision, the reviewer and the outcome go to the audit log.

Tag the same data as customer PII and it gets stricter: the playground's sample policies block it outright. When no policy matches, the score comes from the rule-based risk engine. Policies let you tighten it, for example "any `EXPORT_DATA` to an external destination goes to REVIEW", or "never `DELETE_DATA` outside business hours".

## The audit trail

Every decision is recorded: time, agent, action, resource, decision, reviewer. The audit log is kept as a SHA-256 hash chain that you can verify in the app, and you can download it as JSON or CSV with a SHA-256 checksum. When someone asks "who let the agent do that?", there's an answer.

## Try it

Free plan, no card. Connect Claude Code or Cursor in about two minutes at [app.vulnify.io/setup](https://app.vulnify.io/setup?utm_source=hashnode&utm_medium=referral&utm_campaign=mcp-server-firewall), or check your MCP server's tools with the [free scanner](https://www.vulnify.io/scanner?utm_source=hashnode&utm_medium=referral&utm_campaign=mcp-server-firewall) first. Popular servers already have a public scanner page, like [Hugging Face's](https://www.vulnify.io/scanner/huggingface-co-mcp?utm_source=hashnode&utm_medium=referral&utm_campaign=mcp-server-firewall), with a README badge maintainers can add to their repo.
