Your MCP server's tool calls are silently burning money. Here's a 40-line budget guard.
You have 3 agents running in parallel, each calling the same expensive LLM tool 50 times per minute. The first agent finishes its batch and stops. The second and third agents keep calling until their 10-minute time budget expires. By then you've spent $47 on a job that should have cost $12.
This happens because MCP tool calls are fire-and-forget: you send the request, you get a result, and you never track how much you've spent. No per-call budget, no time limit, no per-agent cap.
Here's a 40-line budget guard that stops runaway tool calls in their tracks.
The problem
An MCP server exposes a tool like this:
{
"name": "llm_generate",
"description": "Generate text using an LLM",
"inputSchema": {
"type": "object",
"properties": {
"prompt": { "type": "string" },
"model": { "type": "string", "enum": ["gpt-4", "claude-3-5-sonnet"] }
}
}
}
Your agent calls it like this:
const result = await client.callTool({
name: "llm_generate",
arguments: { prompt: "Write a Python script to..." }
});
No budget, no time limit, no per-agent cap. If the agent loops 100 times, you pay for 100 calls.
The fix: a budget guard
Wrap the tool call in a budget guard that tracks:
- Time budget (e.g., 10 minutes per agent)
- Token budget (e.g., 10,000 tokens per agent)
- Per-call cost (estimated from the model)
// budget-guard.js
import { EventEmitter } from "events";
class ToolCallBudgetGuard {
constructor(options = {}) {
this.timeBudget = options.timeBudget || 10 * 60 * 1000; // 10 minutes
this.tokenBudget = options.tokenBudget || 10_000;
this.costPerToken = options.costPerToken || 0.00003; // GPT-4 ~$3/1M tokens
this.budgets = new Map(); // agentId -> { timeUsed, tokensUsed, startTime }
this.emitter = new EventEmitter();
}
// Get or create a budget for an agent
getBudget(agentId) {
if (!this.budgets.has(agentId)) {
this.budgets.set(agentId, {
timeUsed: 0,
tokensUsed: 0,
startTime: Date.now()
});
}
return this.budgets.get(agentId);
}
// Check if a tool call is allowed
canCall(agentId, estimatedTokens = 0) {
const budget = this.getBudget(agentId);
const timeUsed = Date.now() - budget.startTime;
const tokensUsed = budget.tokensUsed + estimatedTokens;
const timeExceeded = timeUsed > this.timeBudget;
const tokensExceeded = tokensUsed > this.tokenBudget;
if (timeExceeded || tokensExceeded) {
this.emitter.emit("budgetExceeded", {
agentId,
reason: timeExceeded ? "time" : "tokens",
timeUsed,
tokensUsed
});
return false;
}
return true;
}
// Record a successful tool call
recordCall(agentId, tokens = 0, cost = 0) {
const budget = this.getBudget(agentId);
budget.timeUsed = Date.now() - budget.startTime;
budget.tokensUsed += tokens;
budget.cost += cost;
this.emitter.emit("callRecorded", {
agentId,
tokens,
cost,
timeUsed: budget.timeUsed,
tokensUsed: budget.tokensUsed
});
}
// Get current budget usage
getUsage(agentId) {
const budget = this.budgets.get(agentId);
if (!budget) return null;
return {
timeUsed: Date.now() - budget.startTime,
tokensUsed: budget.tokensUsed,
cost: budget.cost
};
}
}
export default ToolCallBudgetGuard;
Using the guard
import ToolCallBudgetGuard from "./budget-guard.js";
const guard = new ToolCallBudgetGuard({
timeBudget: 10 * 60 * 1000, // 10 minutes
tokenBudget: 10_000,
costPerToken: 0.00003 // GPT-4
});
// Listen for budget exceeded events
guard.on("budgetExceeded", ({ agentId, reason, timeUsed, tokensUsed }) => {
console.error(`[${agentId}] Budget exceeded: ${reason}`);
console.error(` Time used: ${Math.round(timeUsed / 1000)}s`);
console.error(` Tokens used: ${tokensUsed}`);
});
// Wrap your tool call
async function callLLM(agentId, prompt, model = "gpt-4") {
// Estimate tokens (simplified: 4 chars per token for English)
const estimatedTokens = Math.ceil(prompt.length / 4);
// Check budget before calling
if (!guard.canCall(agentId, estimatedTokens)) {
throw new Error(`Tool call blocked: budget exceeded for ${agentId}`);
}
// Call the LLM
const result = await openai.chat.completions.create({
model,
messages: [{ role: "user", content: prompt }]
});
// Record the call
guard.recordCall(agentId, result.usage.total_tokens, result.usage.total_tokens * guard.costPerToken);
return result.choices[0].message.content;
}
// Use it in your agent
const agent1 = callLLM("agent-1", "Write a Python script...");
const agent2 = callLLM("agent-2", "Write a Python script...");
const agent3 = callLLM("agent-3", "Write a Python script...");
What this catches
- Time runaway: If an agent loops 100 times, the guard blocks the 11th call after 10 minutes.
- Token runaway: If an agent generates 20K tokens in a single call, the guard blocks it.
- Per-agent isolation: Each agent has its own budget, so one agent's runaway doesn't affect others.
Where to learn more
- Anthropic's MCP documentation — how MCP tools are defined and called
- OpenAI API pricing — current token costs for GPT-4 and Claude
-
Token estimation libraries — more accurate token counting than
length / 4
Next steps
- Add per-agent budgets — give each agent its own time/token cap.
- Add alerts — send a webhook when a budget is exceeded.
-
Add per-tool budgets — cap specific tools (e.g.,
llm_generate) separately from others.
Runaway tool calls are a silent budget killer. A 40-line guard stops them before they burn you.
This article is a teaching piece. For a production-ready MCP tool-call budgeting pack, see the linked resources above.
Top comments (0)