← Back to Blog

How to Add Spending Controls to Any MCP Agent

Your AI agent just made its 47th Stripe charge of the day. Each one looked reasonable in isolation — $12 here, $35 there — but the cumulative total hit $4,200 before anyone noticed. The agent was doing exactly what it was told: processing orders. It just never stopped.

Adding spending controls to your MCP agent prevents exactly this scenario. MCP servers like Stripe, AWS, and Twilio give agents direct access to tools that cost real money. The agent doesn’t know it has a budget. The MCP server doesn’t enforce one. And the system prompt saying “don’t spend more than $500 per day” is a suggestion, not a constraint.

Intercept solves this by sitting between the agent and the MCP server as a transparent proxy. Every tools/call request passes through it, gets evaluated against a YAML policy file, and is either forwarded or blocked. The agent doesn’t know Intercept exists — same tools, same schemas, same interface.

Here’s how to set it up.

The Architecture

┌──────────┐       ┌───────────┐       ┌────────────┐
│ LLM/AI   │──────>│ Intercept │──────>│ MCP Server │
│ Client   │<──────│  (proxy)  │<──────│ (upstream) │
└──────────┘       └───────────┘       └────────────┘

                   ┌────┴────┐
                   │ Policy  │
                   │ Engine  │
                   └────┬────┘
                   ┌────┴────┐
                   │ State   │
                   │ Store   │
                   └─────────┘

Intercept proxies MCP traffic over stdio or SSE. It intercepts tools/call requests, evaluates them against your policy, and returns a denial message if any rule fails. The state store (SQLite by default, Redis for multi-instance) persists counters across restarts so your daily spend caps survive process recycling.

Step 1: Scan the MCP Server

Before writing policies, you need to know what tools are available. The scan command connects to any MCP server, discovers its tools, and generates a commented YAML scaffold:

intercept scan -o policy.yaml -- npx -y @anthropic/stripe-mcp-server

This produces a file listing every tool with its parameters, grouped by category. It’s a starting point — everything is allowed by default until you add rules.

Step 2: Add a Per-Transaction Limit

The most basic spending control is capping a single transaction. If your agent can call create_charge, you probably don’t want it creating $10,000 charges:

version: "1"
description: "Stripe spending controls"

tools:
  create_charge:
    rules:
      - name: "max single charge"
        conditions:
          - path: "args.amount"
            op: "lte"
            value: 50000
        on_deny: "Single charge cannot exceed $500.00"

This rule checks the amount argument on every create_charge call. If it exceeds 50000 (Stripe uses cents), the call is blocked and the agent receives the denial message. The agent can then decide what to do — ask the user for approval, split the transaction, or abandon the task.

The key detail: this check happens at the transport layer, before the request reaches Stripe. The charge is never created. There’s no refund to process, no failed payment to reconcile. This is deterministic policy enforcement — the same input always produces the same result.

Step 3: Add a Daily Spend Cap

Per-transaction limits don’t prevent accumulation. An agent making 200 charges of $50 each will sail past a $500 single-charge limit while racking up $10,000 in total spend. You need cumulative tracking.

Intercept handles this with stateful counters:

tools:
  create_charge:
    rules:
      - name: "max single charge"
        conditions:
          - path: "args.amount"
            op: "lte"
            value: 50000
        on_deny: "Single charge cannot exceed $500.00"

      - name: "daily spend cap"
        conditions:
          - path: "state.create_charge.daily_spend"
            op: "lte"
            value: 1000000
        on_deny: "Daily spending cap of $10,000.00 reached"
        state:
          counter: "daily_spend"
          window: "day"
          increment_from: "args.amount"

The state block creates a counter called daily_spend that resets at midnight UTC. On each allowed create_charge call, the counter increments by whatever args.amount is. Before the next call, the condition checks whether the cumulative total exceeds the limit.

The increment_from field is what makes this work for spending specifically. Instead of counting calls (the default), it sums the actual dollar amounts. A $50 charge increments by 5000, a $200 charge by 20000. When the running total would exceed 1000000 ($10,000), further charges are denied.

Counters persist in the state store. If you restart Intercept, the daily total picks up where it left off. And the two-phase model means failed upstream calls don’t consume quota — if Stripe returns an error, the increment is rolled back.

Step 4: Restrict Currencies and Arguments

Spending controls aren’t just about amounts. You might want to restrict which currencies an agent can charge in, which regions it can operate in, or which products it can purchase:

      - name: "allowed currencies"
        conditions:
          - path: "args.currency"
            op: "in"
            value: ["usd", "eur"]
        on_deny: "Only USD and EUR charges are permitted"

This uses the in operator to check against a whitelist. You can combine multiple conditions in a single rule — they’re ANDed together:

      - name: "safe charge"
        conditions:
          - path: "args.amount"
            op: "lte"
            value: 50000
          - path: "args.currency"
            op: "in"
            value: ["usd", "eur"]
        on_deny: "Charge must be under $500 and in USD or EUR"

Both conditions must pass. If either fails, the entire call is denied.

Step 5: Block Destructive Operations

Some tools should never be called by an agent, regardless of arguments. Deleting customers, dropping databases, removing infrastructure — these are human-only operations:

hide:
  - delete_customer
  - delete_product
  - delete_invoice

tools:
  delete_subscription:
    rules:
      - name: "block subscription deletion"
        action: "deny"
        on_deny: "Subscription deletion is not permitted via AI agents"

There are two approaches here. The hide list removes tools from the agent’s view entirely — they’re stripped from tools/list responses, so the agent never knows they exist. This saves context window tokens and prevents the agent from even attempting the call.

For tools you want the agent to see but not use, use action: "deny". The tool shows up in tools/list, but any call is unconditionally blocked with the denial message.

Step 6: Add a Global Rate Limit

Even with per-tool spending controls, you want a backstop. A global rate limit caps the total number of tool calls per time window across all tools:

  "*":
    rules:
      - name: "global rate limit"
        rate_limit: 60/minute

The "*" wildcard applies to every tool call. This prevents runaway loops where an agent calls tools hundreds of times per minute, regardless of whether each individual call passes its specific rules. For more on rate limiting strategies, see our practical guide.

Step 7: Wire It Up

With your policy written, run Intercept as a proxy:

intercept -c policy.yaml --upstream https://mcp.stripe.com \
  --header "Authorization: Bearer sk_live_..."

Or for MCP clients that read .mcp.json (Claude Code, Cursor, etc.), point the server config at Intercept:

{
  "mcpServers": {
    "stripe": {
      "command": "intercept",
      "args": [
        "-c", "/path/to/policy.yaml",
        "--",
        "npx", "-y", "@anthropic/stripe-mcp-server"
      ],
      "env": {
        "STRIPE_API_KEY": "sk_live_..."
      }
    }
  }
}

The agent connects to Intercept thinking it’s the Stripe MCP server. Intercept forwards everything except policy violations.

The Complete Policy

Here’s the full policy combining all the rules above:

Click to expand the complete policy YAML
version: "1"
description: "Stripe MCP server spending controls"

hide:
  - delete_customer
  - delete_product
  - delete_invoice

tools:
  create_charge:
    rules:
      - name: "max single charge"
        conditions:
          - path: "args.amount"
            op: "lte"
            value: 50000
        on_deny: "Single charge cannot exceed $500.00"

      - name: "daily spend cap"
        conditions:
          - path: "state.create_charge.daily_spend"
            op: "lte"
            value: 1000000
        on_deny: "Daily spending cap of $10,000.00 reached"
        state:
          counter: "daily_spend"
          window: "day"
          increment_from: "args.amount"

      - name: "allowed currencies"
        conditions:
          - path: "args.currency"
            op: "in"
            value: ["usd", "eur"]
        on_deny: "Only USD and EUR charges are permitted"

  create_refund:
    rules:
      - name: "refund amount cap"
        conditions:
          - path: "args.amount"
            op: "lte"
            value: 10000
        on_deny: "Refunds over $100.00 require manual processing"

      - name: "daily refund count"
        rate_limit: 10/day
        on_deny: "Daily refund limit (10) reached"

  "*":
    rules:
      - name: "global rate limit"
        rate_limit: 60/minute

Hot Reload

Policies are hot-reloadable. Edit the YAML file while Intercept is running and changes apply immediately — no restart, no dropped connections. This means you can tighten limits in response to observed behaviour without interrupting the agent.

You can also validate policies before deploying:

intercept validate -c policy.yaml

This catches syntax errors, invalid operators, missing counters, and logical conflicts before they hit production.

What the Agent Sees

When a call is denied, the agent receives a message like:

[INTERCEPT POLICY DENIED] Daily spending cap of $10,000.00 reached

This is deliberate. The agent knows why the call failed and can adapt its behaviour — inform the user, try a smaller amount, or wait until the window resets. It’s a feedback loop, not a silent failure.

Beyond Stripe

The same pattern works for any MCP server that touches money or resources. AWS cost controls, Twilio message limits, database write caps, API call budgets — if the tool has arguments you can validate and calls you can count, Intercept can enforce limits on it.

FAQ

How do MCP spending controls persist across restarts?

Intercept stores counter state in a persistent state store (SQLite by default, Redis for multi-instance deployments). When you restart Intercept, daily spend totals, rate limit counters, and all other stateful tracking picks up exactly where it left off. No state is lost.

Can MCP agents bypass spending limits?

Not through Intercept. Because spending controls are enforced at the transport layer — between the agent and the MCP server — the agent has no way to bypass them. The agent doesn’t even know Intercept exists. It sees the same tools and schemas, but every tools/call request is evaluated against the policy before reaching the upstream server.

What happens when an MCP agent hits a spending limit?

The agent receives a denial message explaining why the call was blocked, e.g. [INTERCEPT POLICY DENIED] Daily spending cap of $10,000.00 reached. The agent can then adapt — inform the user, try a smaller amount, or wait until the time window resets. The upstream MCP server never receives the blocked request.

Ready to secure your AI agents?

Get spending controls for autonomous agents in 5 minutes.

Get Started