AID-H-034.008: MCP Operation Budget Enforcement Integration

Version 1.20261015 · Actionable sub-technique

Use when an MCP server exposes operations that an authoritative 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.
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

Meter MCP lifecycle deltas and leases, authorize covered effects, and settle only through the reservation-bound authoritative ledger without re-executing retries.
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

Commercial / hosted tools

Threat mappings

MITRE ATLAS

MAESTRO

OWASP LLM Top 10 2026

OWASP ML Top 10 2023

OWASP Top 10 for Agentic Applications 2026

NIST Adversarial Machine Learning 2025

Cisco Integrated AI Security and Safety Framework

Google Secure AI Framework 2.0 - Risks

Databricks AI Security Framework 3.0

AID-H-034.008: MCP Operation Budget Enforcement Integration | AIDEFEND

Loading interactive view…

A Practical Knowledge Base for AI Security Defenses

Version