MCP gives AI agents structured access to tools. What it doesn't give them is a credit card limit.
We shipped an open-source MCP proxy that intercepts tools/call JSON-RPC messages and enforces per-tool budgets with cryptographic delegation. Here's how we built it.
The Architecture
The proxy sits between any MCP client and any MCP server. It speaks standard MCP protocol — agents don't know it's there.
Agent → [stdio/SSE] → SatGate MCP Proxy → [stdio] → Upstream MCP Server
│
Budget Enforcement
Per-tool Cost Attribution
Token DelegationTwo transport modes:
- stdio — Local sidecar. One agent, one process. Zero network overhead.
- SSE/HTTP — Remote server. Multiple agents connect over HTTP, each with an independent SSE event stream.
Per-Tool Cost Resolution
Not all tool calls cost the same. A web_search is cheap. A dalle_generate is expensive. Our cost resolver supports exact match and wildcard prefixes:
tools:
defaultCost: 5
costs:
web_search: 5
database_query: 5
gpt4_summarize: 25
gpt4_*: 25 # wildcard: gpt4_analyze, gpt4_translate...
dalle_generate: 50
code_execute: 15Resolution order: exact match → longest wildcard prefix → catch-all * → default. Same pattern as the enterprise cost attribution engine, but running locally.
The BudgetEnforcer Interface
This is the split point between OSS and Enterprise:
type BudgetEnforcer interface {
Check(ctx, tokenID string, cost int64) (*BudgetResult, error)
Spend(ctx, tokenID, toolName string, cost int64, requestID string) (*BudgetResult, error)
Remaining(ctx, tokenID string) (int64, error)
Initialize(ctx, tokenID string, credits int64) error
}OSS provides InMemoryBudgetEnforcer — a mutex-protected map. Simple, fast, not durable across restarts. Enterprise provides RedisBudgetEnforcer — atomic Lua scripts, idempotent spend tracking, Postgres audit trail.
When budget hits zero:
{"jsonrpc":"2.0","id":42,"error":{
"code":-32000,
"message":"Budget exhausted",
"data":{
"error":"budget_exhausted",
"tool":"dalle_generate",
"cost_credits":50,
"remaining_credits":0
}
}}The agent gets a structured error it can handle gracefully — not a crashed process or an infinite retry.
Delegation: The Hard Part
When an orchestrator agent spawns sub-agents, each needs its own budget. The parent carves credits from its own allocation:
Orchestrator (1000 credits)
├── satgate/delegate(300, "research-agent") → child token
├── satgate/delegate(200, "content-agent") → child token
└── 500 credits remaining
Result:
research-agent: 60 calls → 402 EXHAUSTED
content-agent: still operational ✓
orchestrator: 500 credits remaining ✓The key design decision: token identity = hash(identifier + signature). Macaroon delegation produces child tokens with the same identifier as the parent (that's how HMAC chaining works). But each delegation adds caveats that change the signature, making the hash unique. This gives us a stable budget key per token without requiring a separate identity system.
What We Learned
Macaroons are underrated for agent auth. HMAC chain means verification is constant-time with no database lookup. Delegation is just appending caveats. Permissions can only narrow, never widen. Perfect for agent-to-agent delegation.
stdio transport is simpler than you'd think. Newline-delimited JSON over stdin/stdout. No HTTP overhead. The upstream manager spawns the MCP server as a subprocess and correlates request/response IDs via a sync.Map of pending channels.
SSE needs keepalive. Connections through load balancers drop after 30-60s of silence. A periodic SSE comment line prevents this. Also: make your message handler async — if tool calls block the HTTP response goroutine, you get head-of-line blocking across sessions.
Fail-mode matters. When the budget backend is unreachable, deny all (closed) or allow and log (open)? We default to closed — secure first. But making it configurable was worth it.
By the Numbers
- 18 source files, 2,164 lines of Go
- 10 test files, 1,365 lines of tests
- 28 tests: budget, auth, delegation, config, JSON-RPC, SSE, integration
- 63% test-to-source ratio
The code is open source. Try it:
go install github.com/satgate-io/satgate/cmd/satgate-mcp@latest