NullSpend Docs

MCP Proxy

MCP proxy that gates risky tool calls through NullSpend approval before forwarding to an upstream MCP server. Adds cost tracking and budget enforcement for ever

MCP proxy that gates risky tool calls through NullSpend approval before forwarding to an upstream MCP server. Adds cost tracking and budget enforcement for every tool invocation.

Architecture

LLM Client ──► NullSpend MCP Proxy ──► Upstream MCP Server
                    │                         │
                    │  ◄── tool result ───────┘

                    ├──► NullSpend API (approval, cost events, budget checks)


              NullSpend Dashboard

The proxy sits between the LLM client and the upstream MCP server. It discovers upstream tools at startup, optionally gates tool calls through human approval, tracks cost per invocation, and enforces budgets.

Installation

Build from source (private package):

cd packages/mcp-proxy
npm install
npm run build

Configuration

VariableRequiredDefaultDescription
NULLSPEND_URLYesNullSpend dashboard URL (e.g. https://nullspend.dev)
NULLSPEND_API_KEYYesAPI key (ns_live_sk_... or ns_test_sk_...)
UPSTREAM_COMMANDYesCommand to start the upstream MCP server
UPSTREAM_ARGSNo[]JSON array of arguments for the upstream command
UPSTREAM_ENVNo{}JSON object of additional env vars for the upstream process
GATED_TOOLSNo"*"Which tools require approval (see Gating)
PASSTHROUGH_TOOLSNo""Tools that always skip approval
NULLSPEND_AGENT_IDNo"mcp-proxy"Agent ID for created actions
APPROVAL_TIMEOUT_SECONDSNo300Seconds to wait for human approval
NULLSPEND_COST_TRACKINGNo"true"Set to "false" to disable cost event reporting
NULLSPEND_BUDGET_ENFORCEMENTNo"true"Set to "false" to disable budget checks
NULLSPEND_SERVER_NAMENoUPSTREAM_COMMANDServer name for cost events and analytics. Must not contain /
NULLSPEND_TOOL_COSTSNo{}JSON object mapping tool names to cost in microdollars

Gating

Tool gating controls which upstream tools require human approval before execution.

How It Works

  1. If a tool is in PASSTHROUGH_TOOLS, it is never gated (passthrough always wins)
  2. If GATED_TOOLS is "*", all non-passthrough tools require approval
  3. If GATED_TOOLS is a comma-separated list, only those tools require approval
  4. If GATED_TOOLS is "" (empty), no tools require approval

Examples

GATED_TOOLSPASSTHROUGH_TOOLSEffect
*(empty)All tools gated
*read_file,list_dirAll tools gated except read_file and list_dir
write_file,delete_file(empty)Only write_file and delete_file gated
(empty string)(empty)No tools gated (approval disabled, cost tracking still active)

Approval Flow

When a gated tool is called:

  1. The proxy creates an action via POST /api/actions with the tool name, arguments, and a summary
  2. It polls for a human decision (approved/rejected/expired)
  3. On approval: forwards the call to the upstream server, then reports the result
  4. On rejection: returns an error to the LLM client: Action "<tool>" was rejected by a human reviewer.
  5. On timeout: returns an error: Approval for "<tool>" timed out after N seconds.

Cost Tracking

The proxy tracks cost for every tool invocation (gated or not) when NULLSPEND_COST_TRACKING is "true" (default).

Cost Estimation

Cost per tool call is estimated using a three-tier priority system:

PrioritySourceDescription
1NULLSPEND_TOOL_COSTS env varPer-tool overrides in microdollars
2Dashboard-configured costsFetched from /api/tool-costs at startup
3MCP annotation tiersInferred from the tool's annotations field

Annotation Tiers (Suggested Costs)

Tools are unpriced ($0.00) by default until you configure a cost. The dashboard shows suggested costs based on MCP annotations to help you set appropriate prices:

ConditionTierSuggested Cost
readOnlyHint: true AND openWorldHint: falseFREE$0.00
destructiveHint: true AND openWorldHint: trueWRITE$0.10
Everything else (default)READ$0.01

These are suggestions only — they are not auto-applied. Accept or override them in the dashboard's Tool Costs page.

To set costs for specific tools via environment variable:

NULLSPEND_TOOL_COSTS='{"write_file": 50000, "run_query": 200000}'

Values are in microdollars (1,000,000 = $1.00).

Budget Enforcement

When NULLSPEND_BUDGET_ENFORCEMENT is "true" (default), the proxy checks the budget before each tool call:

  1. Estimates the cost using the priority system above
  2. Calls POST /v1/mcp/budget/check with the tool name, server name, and estimate
  3. If the budget is exceeded, returns an error: Tool "<name>" blocked: budget exceeded.
  4. If the check fails (network error, timeout), falls back to fail-open after 5 consecutive failures (circuit breaker with 30s cooldown)

Event Reporting

Cost events are batched and sent to POST /v1/mcp/events:

  • Batch size: 20 events
  • Flush interval: 5 seconds
  • Max queue: 4,096 events (oldest dropped on overflow)
  • Failed batches are re-queued once for retry

Tool Discovery

At startup, the proxy registers all upstream tools with the dashboard via POST /api/tool-costs/discover. This populates the tool catalog for per-tool cost configuration in the UI.

Claude Desktop Setup

Example claude_desktop_config.json with gating and passthrough:

{
  "mcpServers": {
    "gated-filesystem": {
      "command": "node",
      "args": ["path/to/packages/mcp-proxy/dist/index.js"],
      "env": {
        "NULLSPEND_URL": "https://nullspend.dev",
        "NULLSPEND_API_KEY": "ns_live_sk_your-key-here",
        "UPSTREAM_COMMAND": "npx",
        "UPSTREAM_ARGS": "[\"@modelcontextprotocol/server-filesystem\", \"/home/user/projects\"]",
        "GATED_TOOLS": "write_file,delete_file",
        "PASSTHROUGH_TOOLS": "read_file,list_dir",
        "NULLSPEND_SERVER_NAME": "filesystem"
      }
    }
  }
}

Gate-Everything Example

{
  "mcpServers": {
    "gated-everything": {
      "command": "node",
      "args": ["path/to/packages/mcp-proxy/dist/index.js"],
      "env": {
        "NULLSPEND_URL": "https://nullspend.dev",
        "NULLSPEND_API_KEY": "ns_live_sk_your-key-here",
        "UPSTREAM_COMMAND": "npx",
        "UPSTREAM_ARGS": "[\"some-mcp-server\"]",
        "NULLSPEND_TOOL_COSTS": "{\"expensive_tool\": 500000}"
      }
    }
  }
}

Error Messages

ScenarioError Text
Budget exceededTool "<name>" blocked: budget exceeded. Remaining: <n> microdollars.
Action rejectedAction "<name>" was rejected by a human reviewer.
Approval timeoutApproval for "<name>" timed out after <n> seconds. The action was not executed.
Upstream errorUpstream error: <message>
Upstream error after approvalUpstream call failed after approval: <message>
Approval service unreachableFailed to reach approval service: <message>

Graceful Shutdown

The proxy handles SIGINT, SIGTERM, and stdin close. On shutdown:

  1. Aborts any in-flight approval polls
  2. Flushes remaining cost events (no re-queue during shutdown)
  3. Waits for in-flight HTTP requests to complete
  4. Closes the upstream MCP client connection

On this page