Blog

Runtime authorization and audit for any MCP server in 2 minutes

· 5 min read

Vulnify blog cover: Runtime authorization and audit for any MCP server in 2 minutes

An MCP server hands your agent a set of tools. Most are harmless: list_issues, get_customer, search_docs. A few are not: export_contacts, delete_records, send_email, create_payment. The protocol itself doesn't decide which calls are fine to run right now. Whatever the model asks for, the server executes.

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 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:

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:

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):

{
  "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).

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:

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.

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.

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 (action EXPORT, 800 records, external email destination, data not tagged as customer PII) and the risk engine answers:

{
  "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, or check your MCP server's tools with the free scanner first. Popular servers already have a public scanner page, like Hugging Face's, with a README badge maintainers can add to their repo.

Vulnify decides every sensitive agent action before it runs: ALLOW, REVIEW or BLOCK, with the reasons.

Try it free