Your MCP Server Needs a Capability Budget, Not Just Auth
Most MCP security checklists stop at “is this caller authenticated?” That is necessary, but it does not answer the operational question: what is this tool allowed to do during this run? A useful boundary is a capability budget: a short-lived, explicit contract for each tool invocation. It should constrain the action, target, resource, quantity, and expiry. The model can request a tool, but the runtime decides whether the request fits the contract. 1. Define the budget next to the tool Start with a deliberately boring schema: type CapabilityBudget = { runId: string; tool: string; actions: string[]; resources: string[]; maxCalls: number; maxBytes?: number; expiresAt: string; approval: "none" | "human"; policyVersion: string; }; For example, a code-review run might receive: { "runId": "run_8f2", "tool": "github.create_comment", "actions": ["comment"], "resources": ["repo:acme/api:pull:481"], "maxCalls": 1, "expiresAt": "2026-08-14T03:00:00Z", "approval": "human", "policyVersion": "p17" } The important part is what is absent: no repository-wide write permission, no issue creation, and no unbounded retry allowance. 2. Enforce it at dispatch time Do not check the budget only when the agent plans the call. The queue, worker, and tool adapter should all treat the budget as untrusted input and revalidate it. A dispatch decision can be reduced to: Load the current run and budget. Verify the budget is unexpired. Verify the requested action and resource match exactly. Atomically reserve one call and any byte quota. Recheck the current policy and credential version. Dispatch with the reservation ID. If steps 4 and 5 cannot be made durable, a worker crash can turn one approved call into several attempts. That is a reliability bug as well as a security bug. 3. Separate planning from spending Let the model propose a sequence, but make the runtime spend from the budget one reservation at a time. A plan such as “inspect, patch, test, notify” should not silently inherit write capability from the first step. A minimal ledger makes this visible: reservation tool requested decision outcome r1 repo.read 1 call allowed confirmed r2 repo.write 1 call approval required pending r3 slack.send 1 call denied not dispatched Keep pending and unknown distinct. Pending means no dispatch has been recorded. Unknown means dispatch may have happened but confirmation was lost. Only the latter requires provider lookup or an idempotency-key reconciliation before retry. 4. Test the failure modes A capability budget is only real if it survives interruptions. Inject at least these cases: The worker pauses after reservation but before dispatch. The provider times out after accepting the request. The policy changes while a job is queued. The model changes the resource identifier between planning and dispatch. Two workers race for the last allowed call. A retry arrives after the budget expires. A child tool asks for a broader capability than its parent run owns. For every case, record the expected ledger state, whether a provider lookup is required, and whether a human must reapprove. A green test suite that never forces an unknown outcome is testing the happy path, not the boundary. 5. Make the hosting boundary explicit If this runtime must stay available for queued work or browser-assisted tasks, hosting is part of the control plane. A managed runtime such as managed OpenClaw hosting on Ampere can be evaluated as one deployment option, but it does not replace capability checks, prompt-injection defenses, credential scoping, or reconciliation logic. The questions to verify are practical: where durable run state lives, how workers restart, how credentials are mounted, how logs are retained, and how you rebuild the same policy version on a clean host. Treat the hosting choice as an availability and recovery decision, not as an authorization decision. A compact acceptance checklist Before calling an MCP integration production-ready, verify: Every tool call has a run-bound, expiring capability budget. Action and resource are matched at dispatch, not only during planning. Call and byte limits are reserved atomically. Policy and credential versions are rechecked before execution. Parent and child tools cannot expand one another’s authority. Timeouts produce unknown, not an automatic retry. Provider IDs and idempotency keys support reconciliation. Expiry, revocation, and clean-host restore are tested. Authentication answers “who is asking?” A capability budget adds “what may happen, where, how often, and until when?” That is the boundary worth reviewing.
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to