AID-H-034.008: MCP Operation Budget Enforcement Integration
AID-I-003.003 quota or spend policy meters by request, concurrency lease, Task or stream lifetime, incremental tokens or bytes, tool invocation, or protected backend effect. Translate authenticated MCP principal, client, tenant, method, tool, resource, Task, subscription, and idempotency context into the authoritative ledger's reservation, incremental-usage, lease-renewal, and settlement API; enforce its decision before dispatch and immediately before covered effects; and preserve UNKNOWN effects for reconciliation.Pillars: app, infra
Lifecycle: building, operation
Scope boundary
Owns MCP-specific translation of authenticated request, tool, resource, task, subscription, idempotency, and effect context into complete authoritative-ledger reservation, metering, lease, commit, settlement, retry, and reconciliation hooks.
AID-I-003.003: AID-H-034.008 integrates MCP handlers with budget decisions at every required lifecycle hook; AID-I-003.003 owns policy dimensions, thresholds, counters, leases, accounting, rollback protection, and the authoritative ledger. The MCP integration consumes ledger authority and must not maintain a competing budget truth.
AID-D-005.007: AID-H-034.008 prevents or bounds MCP operation consumption using authoritative decisions; AID-D-005.007 detects abnormal token, tool-use, parameter, and cost patterns. Anomaly detection does not reserve or settle a budget.
Implementation guidance
- AID-H-034.008-G001: Translate authenticated MCP context into a separate idempotency identity and operation digest, then enforce the AID-I-003.003 authoritative ledger decision before work.
- AID-H-034.008-G002: Meter MCP lifecycle deltas and leases, authorize covered effects, and settle only through the reservation-bound authoritative ledger without re-executing retries.
AID-H-034.008-G001
Applicability and ownership
Use this adapter when the authoritative AID-I-003.003 ledger selects an MCP operation for metering. The MCP server supplies authenticated protocol context and enforces the returned decision; it does not define limits, counter scopes, settlement modes, policy generations, or quota storage. The ledger must distinguish fixed consumption, incremental usage, renewable leases, and protected-effect reservations.
Reserve with separate idempotency and content bindings
Derive the stable idempotency identity from the authenticated scope plus JSON-RPC request ID. Derive the canonical operation digest separately from the method, tool or resource name, arguments, Task or object identity, and server release. The ledger must return a conflict when the same idempotency identity arrives with different operation bytes.
// File: src/budget/mcpBudgetAdapter.js
import crypto from "node:crypto";
import fs from "node:fs";
import https from "node:https";
const ACTIONS = new Set([
"EXECUTE_NEW",
"DO_NOT_EXECUTE_RECONCILE",
"RETURN_COMMITTED",
"RETURN_NO_EFFECT",
"DENY"
]);
const canonical = value => {
if (
value === null || typeof value === "string"
|| typeof value === "boolean"
) return JSON.stringify(value);
if (Number.isSafeInteger(value)) return String(value);
if (Array.isArray(value)) {
return "[" + value.map(canonical).join(",") + "]";
}
if (value && Object.getPrototypeOf(value) === Object.prototype) {
return "{" + Object.keys(value).sort().map(
key => JSON.stringify(key) + ":" + canonical(value[key])
).join(",") + "}";
}
throw new Error("non_canonical_mcp_operation_value");
};
const exactKeys = (value, expected) =>
value && Object.getPrototypeOf(value) === Object.prototype
&& Object.keys(value).sort().join(",")
=== [...expected].sort().join(",");
const decodeBase64 = value => {
if (typeof value !== "string" || !/^[A-Za-z0-9+/]+={0,2}$/.test(value)) {
throw new Error("invalid_ledger_envelope_base64");
}
const decoded = Buffer.from(value, "base64");
if (!decoded.length || decoded.toString("base64") !== value) {
throw new Error("non_canonical_ledger_envelope_base64");
}
return decoded;
};
function positiveMilliseconds(name) {
const value = Number(process.env[name]);
if (!Number.isSafeInteger(value) || value <= 0) {
throw new Error("invalid_required_timeout:" + name);
}
return value;
}
function loadHexKey(name) {
const value = Buffer.from(process.env[name] || "", "hex");
if (value.length < 32) throw new Error("invalid_digest_key:" + name);
return value;
}
function postHttps(url, options, body) {
return new Promise((resolve, reject) => {
const request = https.request(url, {
method: "POST",
agent: options.agent,
headers: options.headers,
timeout: options.timeoutMs
}, response => {
const chunks = [];
let received = 0;
response.on("data", chunk => {
received += chunk.length;
if (received > options.maxResponseBytes) {
request.destroy(new Error("budget_ledger_response_too_large"));
return;
}
chunks.push(chunk);
});
response.on("end", () => resolve({
statusCode: response.statusCode || 0,
body: Buffer.concat(chunks)
}));
});
request.on("timeout", () =>
request.destroy(new Error("budget_ledger_timeout"))
);
request.on("error", reject);
request.end(body);
});
}
function verifyLedgerEnvelope(rawEnvelope, publicKey, expected) {
const envelope = JSON.parse(rawEnvelope.toString("utf8"));
if (!exactKeys(envelope, ["payload_b64", "signature_b64"])) {
throw new Error("ledger_envelope_schema_differs");
}
const payloadBytes = decodeBase64(envelope.payload_b64);
const signature = decodeBase64(envelope.signature_b64);
if (!crypto.verify(null, payloadBytes, publicKey, signature)) {
throw new Error("ledger_response_signature_invalid");
}
const payload = JSON.parse(payloadBytes.toString("utf8"));
const fields = [
"action", "expires_at", "idempotency_key", "ledger_policy_digest",
"ledger_policy_generation", "operation_digest", "required_hooks",
"reservation_id", "schema_version", "state"
];
if (
!exactKeys(payload, fields)
|| payload.schema_version !== "aidefend.budget-reservation.v1"
|| payload.idempotency_key !== expected.idempotencyKey
|| payload.operation_digest !== expected.operationDigest
|| !ACTIONS.has(payload.action)
|| !Number.isSafeInteger(payload.ledger_policy_generation)
|| payload.ledger_policy_generation <= 0
|| !/^[a-f0-9]{64}$/.test(payload.ledger_policy_digest)
|| !Array.isArray(payload.required_hooks)
|| payload.required_hooks.length
!== new Set(payload.required_hooks).size
|| !Number.isFinite(Date.parse(payload.expires_at))
|| Date.parse(payload.expires_at) <= Date.now()
) {
throw new Error("ledger_reservation_binding_invalid");
}
if (
payload.action !== "DENY"
&& (typeof payload.reservation_id !== "string"
|| !payload.reservation_id)
) {
throw new Error("ledger_reservation_id_missing");
}
return payload;
}
export class McpBudgetAdapter {
constructor() {
this.baseUrl = new URL(process.env.MCP_BUDGET_LEDGER_URL);
if (this.baseUrl.protocol !== "https:") {
throw new Error("budget ledger must use HTTPS");
}
this.timeoutMs = positiveMilliseconds(
"MCP_BUDGET_LEDGER_TIMEOUT_MS"
);
this.digestKey = loadHexKey("MCP_BUDGET_CONTEXT_HMAC_KEY_HEX");
this.digestKeyVersion = process.env.MCP_BUDGET_CONTEXT_KEY_VERSION;
if (!this.digestKeyVersion) {
throw new Error("budget context digest-key version is required");
}
this.ledgerPublicKey = crypto.createPublicKey(
fs.readFileSync(process.env.MCP_BUDGET_LEDGER_VERIFY_KEY)
);
this.maxResponseBytes = positiveMilliseconds(
"MCP_BUDGET_LEDGER_MAX_RESPONSE_BYTES"
);
this.agent = new https.Agent({
ca: fs.readFileSync(process.env.MCP_BUDGET_LEDGER_CA_FILE),
cert: fs.readFileSync(process.env.MCP_BUDGET_CLIENT_CERT),
key: fs.readFileSync(process.env.MCP_BUDGET_CLIENT_KEY),
minVersion: "TLSv1.2"
});
}
hmac(label, value) {
return crypto.createHmac("sha256", this.digestKey)
.update(label)
.update(Buffer.from([0]))
.update(canonical(value))
.digest("hex");
}
async post(pathname, body, expected) {
const url = new URL(pathname, this.baseUrl);
if (url.origin !== this.baseUrl.origin) {
throw new Error("budget ledger origin changed");
}
const response = await postHttps(url, {
agent: this.agent,
headers: {"content-type": "application/json"},
timeoutMs: this.timeoutMs,
maxResponseBytes: this.maxResponseBytes
}, canonical(body));
const raw = response.body;
if (response.statusCode === 409) {
throw new Error("budget_idempotency_content_conflict");
}
if (response.statusCode !== 200) {
throw new Error("budget_ledger_http_" + response.statusCode);
}
return verifyLedgerEnvelope(
raw, this.ledgerPublicKey, expected
);
}
async reserve(input) {
const auth = input.auth;
const context = input.context;
if (
!auth?.subject || !auth?.activeTenantId || !context?.clientId
|| input.requestId === null || input.requestId === undefined
|| !context.serverRelease || !input.method
) {
throw new Error("authenticated_mcp_budget_context_missing");
}
const operationName = input.method
+ (input.name ? ":" + input.name : "");
const idempotencyKey = this.hmac("idempotency", {
tenant: auth.activeTenantId,
subject: auth.subject,
client_id: context.clientId,
request_id: input.requestId,
operation: operationName
});
const operationDigest = this.hmac("operation", {
tenant: auth.activeTenantId,
subject: auth.subject,
client_id: context.clientId,
request_id: input.requestId,
method: input.method,
name: input.name || null,
arguments: input.arguments || null,
task_id: context.taskId || null,
subscription_id: context.subscriptionId || null,
object_id: context.objectId || null,
server_release: context.serverRelease
});
const body = {
schema_version: "aidefend.mcp-budget-admission.v1",
context_digest_key_version: this.digestKeyVersion,
idempotency_key: idempotencyKey,
operation_digest: operationDigest,
operation: operationName,
tenant_id: auth.activeTenantId,
subject_id: auth.subject,
client_id: context.clientId,
task_id: context.taskId || null,
subscription_id: context.subscriptionId || null,
object_id: context.objectId || null,
server_release: context.serverRelease
};
const reservation = await this.post(
"/v1/mcp/reservations",
body,
{idempotencyKey, operationDigest}
);
if (reservation.action === "DENY") {
throw new Error("mcp_budget_denied");
}
return {
...reservation,
idempotencyKey,
operationDigest
};
}
}
Enforce retry state before invoking work
// File: src/budget/reservationDecision.js
export function reservationDecision(reservation) {
switch (reservation.action) {
case "EXECUTE_NEW":
return {execute: true, terminal: null};
case "DO_NOT_EXECUTE_RECONCILE":
return {execute: false, terminal: "reconcile"};
case "RETURN_COMMITTED":
return {execute: false, terminal: "committed"};
case "RETURN_NO_EFFECT":
return {execute: false, terminal: "no_effect"};
default:
throw new Error("unusable_budget_reservation_action");
}
}
Ledger contract required from AID-I-003.003: The authoritative service must reject policy-generation rollback, preserve active-window usage across policy rotation, keep reservations at least through the maximum execution and reconciliation horizon, bind the original policy digest, dimension rules, backend identity, and receipt keys to the reservation, and return the prior state for identical retries. Same idempotency identity plus different canonical content is a conflict. An existing, pending, committed, or no-effect attempt is never executed again under the same protocol request ID.
Verify safely
Exercise all policy-selected MCP methods and tools with identical retries, same request ID with changed arguments, cross-tenant/client/tool reuse, policy rotation and rollback, replica clock skew, ledger failover, stale response signatures, and every returned action. Confirm the MCP handler invokes work only for EXECUTE_NEW and that unknown ledger state blocks selected privileged or state-changing operations.
AID-H-034.008-G002
Lifecycle hooks and settlement boundary
After admission, honor every lifecycle hook returned by the authoritative ledger. Fixed request consumption is never refunded. Token and byte usage is appended as measured deltas. Concurrency, Task, and stream dimensions use renewable leases. Only a reservation explicitly marked by the ledger as REFUND_ON_SIGNED_NO_EFFECT may be released after the ledger verifies an authoritative backend NO_EFFECT receipt. A timeout, exception, disconnect, missing receipt, or unavailable ledger remains pending reconciliation.
Meter, renew, authorize effects, and settle through the same ledger
// File: src/budget/mcpBudgetLifecycle.js
import { reservationDecision } from "./reservationDecision.js";
const SUPPORTED_HOOKS = new Set([
"INCREMENTAL_TOKENS",
"INCREMENTAL_BYTES",
"LEASE_RENEWAL",
"PRE_BACKEND_EFFECT"
]);
function requirePositiveInteger(value, field) {
if (!Number.isSafeInteger(value) || value <= 0) {
throw new Error("invalid_" + field);
}
return value;
}
async function ledgerTransition(adapter, reservation, path, body) {
return adapter.post(
path,
{
schema_version: "aidefend.mcp-budget-transition.v1",
reservation_id: reservation.reservation_id,
idempotency_key: reservation.idempotencyKey,
operation_digest: reservation.operationDigest,
...body
},
{
idempotencyKey: reservation.idempotencyKey,
operationDigest: reservation.operationDigest
}
);
}
export function createBudgetHooks(adapter, reservation) {
const unknown = reservation.required_hooks.filter(
hook => !SUPPORTED_HOOKS.has(hook)
);
if (unknown.length) {
throw new Error("unsupported_required_budget_hooks:" + unknown.join(","));
}
let usageSequence = 0;
return {
async recordTokens(units) {
if (!reservation.required_hooks.includes("INCREMENTAL_TOKENS")) {
throw new Error("token_metering_not_selected");
}
return ledgerTransition(
adapter,
reservation,
"/v1/mcp/reservations/usage",
{
sequence: ++usageSequence,
measurement: "tokens",
units: requirePositiveInteger(units, "token_units")
}
);
},
async recordBytes(units) {
if (!reservation.required_hooks.includes("INCREMENTAL_BYTES")) {
throw new Error("byte_metering_not_selected");
}
return ledgerTransition(
adapter,
reservation,
"/v1/mcp/reservations/usage",
{
sequence: ++usageSequence,
measurement: "bytes",
units: requirePositiveInteger(units, "byte_units")
}
);
},
async renewLease(leaseSequence) {
if (!reservation.required_hooks.includes("LEASE_RENEWAL")) {
throw new Error("lease_renewal_not_selected");
}
return ledgerTransition(
adapter,
reservation,
"/v1/mcp/reservations/lease",
{
lease_sequence: requirePositiveInteger(
leaseSequence, "lease_sequence"
)
}
);
},
async authorizeBackendEffect(effectDigest) {
if (!reservation.required_hooks.includes("PRE_BACKEND_EFFECT")) {
throw new Error("backend_effect_hook_not_selected");
}
if (!/^[a-f0-9]{64}$/.test(effectDigest)) {
throw new Error("backend_effect_digest_invalid");
}
return ledgerTransition(
adapter,
reservation,
"/v1/mcp/reservations/effects",
{effect_digest: effectDigest}
);
}
};
}
async function markUnknown(adapter, reservation, reasonClass) {
return ledgerTransition(
adapter,
reservation,
"/v1/mcp/reservations/unknown",
{reason_class: reasonClass}
);
}
async function settleTerminal(
adapter, reservation, rawTerminalEnvelope
) {
if (!Buffer.isBuffer(rawTerminalEnvelope) || !rawTerminalEnvelope.length) {
throw new Error("authoritative_terminal_receipt_missing");
}
return ledgerTransition(
adapter,
reservation,
"/v1/mcp/reservations/settle",
{
backend_terminal_envelope_b64:
rawTerminalEnvelope.toString("base64")
}
);
}
async function reconcileExisting(adapter, reservation, input) {
let rawTerminalEnvelope;
try {
rawTerminalEnvelope = await input.readAuthoritativeTerminalReceipt({
idempotencyKey: reservation.idempotencyKey,
reservationId: reservation.reservation_id
});
} catch {
return markUnknown(
adapter, reservation, "backend_reconciliation_unavailable"
);
}
if (!Buffer.isBuffer(rawTerminalEnvelope)) {
return markUnknown(
adapter, reservation, "backend_terminal_state_unknown"
);
}
return settleTerminal(adapter, reservation, rawTerminalEnvelope);
}
export async function executeWithBudget(adapter, input) {
const reservation = await adapter.reserve(input);
const decision = reservationDecision(reservation);
if (!decision.execute) {
if (decision.terminal === "committed") {
const committed = await input.readCommittedResult({
idempotencyKey: reservation.idempotencyKey,
reservationId: reservation.reservation_id
});
if (committed === undefined) {
throw new Error("committed_result_unavailable_do_not_reexecute");
}
return {value: committed, reservation, replayed: true};
}
if (decision.terminal === "no_effect") {
return {
value: null,
reservation,
replayed: true,
terminalState: "NO_EFFECT"
};
}
const reconciliation = await reconcileExisting(
adapter, reservation, input
);
return {
value: null,
reservation,
replayed: true,
terminalState: reconciliation.state
};
}
const hooks = createBudgetHooks(adapter, reservation);
let response;
try {
response = await input.invokeBackend({
idempotencyKey: reservation.idempotencyKey,
reservationId: reservation.reservation_id,
budgetHooks: hooks
});
} catch (error) {
try {
await markUnknown(
adapter, reservation, "backend_invocation_exception"
);
} catch {
error.budgetSettlementUnknown = true;
}
throw error;
}
if (!Buffer.isBuffer(response?.signedTerminalEnvelope)) {
await markUnknown(
adapter, reservation, "backend_terminal_receipt_missing"
);
throw new Error("backend_terminal_receipt_missing");
}
const settlement = await settleTerminal(
adapter, reservation, response.signedTerminalEnvelope
);
if (
!["SETTLED_COMMITTED", "SETTLED_NO_EFFECT"].includes(
settlement.state
)
) {
throw new Error("budget_terminal_settlement_not_established");
}
return {
value: response.value,
reservation,
settlement,
replayed: false
};
}
Authoritative-ledger requirements: AID-I-003.003 must use server time, independent per-scope and per-operation namespaces, atomic multidimensional admission, dimension-specific settlement modes, monotonic policy generations, rotation rules that preserve active usage, renewable fenced leases, incremental usage sequences, and reservation-bound backend/key trust. It must cache or reference terminal results sufficiently to prevent committed retries from re-executing. H-034.008 consumes those guarantees and must not implement a second counter store.
Verify safely
Exercise fixed request use plus refundable effect reservation, incremental token and byte deltas, concurrency and Task/stream lease expiry, backend commit-before-timeout, timeout-before-commit, signed NO_EFFECT, invalid and wrong-backend receipts, ledger outage during each transition, duplicate usage sequence, identical retry in every terminal state, and process restart. Confirm only the ledger applies refunds, fixed consumption remains charged after NO_EFFECT, a committed or pending attempt never re-executes, and reconciliation cannot create capacity.
Open-source tools
- Open Policy Agent (OPA)
- OpenMeter
Commercial / hosted tools
- OpenMeter Cloud
Threat mappings
MITRE ATLAS
- AML.T0029 Denial of AI Service
- AML.T0034.002 Cost Harvesting: Agentic Resource Consumption
MAESTRO
- Denial of Service (DoS) Attacks (L1)
- Denial of Service (DoS) Attacks (L4)
OWASP LLM Top 10 2026
- LLM06:2026 Unbounded Consumption
OWASP ML Top 10 2023
- N/A
OWASP Top 10 for Agentic Applications 2026
- ASI08:2026 Cascading Failures
NIST Adversarial Machine Learning 2025
- NISTAML.014 Energy-latency
Cisco Integrated AI Security and Safety Framework
- AISubtech-13.1.1 Compute Exhaustion (enforces authoritative MCP budget decisions at all policy-selected lifecycle hooks)
- AISubtech-13.1.2 Memory Flooding (enforces authoritative MCP budget decisions at all policy-selected lifecycle hooks)
- AISubtech-13.1.3 Model Denial of Service (enforces authoritative MCP budget decisions at all policy-selected lifecycle hooks)
- AISubtech-13.1.4 Application Denial of Service (enforces authoritative MCP budget decisions at all policy-selected lifecycle hooks)
- AISubtech-13.2.1 Service Misuse for Cost Inflation (enforces authoritative MCP budget decisions at all policy-selected lifecycle hooks)
- AISubtech-18.2.1 Abuse of APIs for Mass Automation (Bounds bulk MCP operations through authoritative per-scope request and concurrency budgets)
- AISubtech-4.3.4 Replay Exploitation
- AITech-13.1 Disruption of Availability (enforces authoritative MCP budget decisions at all policy-selected lifecycle hooks)
- AITech-13.2 Cost Harvesting / Repurposing (enforces authoritative MCP budget decisions at all policy-selected lifecycle hooks)
- AITech-4.3 Protocol Manipulation
Google Secure AI Framework 2.0 - Risks
- DMS: Denial of ML Service
Databricks AI Security Framework 3.0
- Model Serving - Inference requests 9.7: Denial of Service (DoS)