The permission review looked reassuring:

METHOD          report.preview
CLASS           READ
APPROVAL        NOT REQUIRED
CALLS           186
WRITE CALLS     0

The warehouse invoice and audit log told a different story:

PAID QUERIES            186
EXPORT OBJECTS          186
OUTBOUND WEBHOOKS       186
REPORTS MARKED VIEWED   186
CUSTOMER ROWS TOUCHED   2,418,906

report.preview did not update a business record directly. Each call still started a paid warehouse query, wrote an export to object storage, sent a webhook, and changed the report’s audit state.

The method name described the interface. It did not describe the effects.

Method effect contract for a Claude Code MCP tool

Read and write are too small for production permissions

A source-code read is usually local and cheap. A production read may retrieve regulated data, populate a cache, reserve capacity, reveal that a customer record exists, or trigger work elsewhere.

HTTP verbs do not fix this. A GET endpoint can start a job. A GraphQL query can generate an export. An MCP method called preview can create stored data and emit an event.

Classify the method by what its full execution path can cause:

effect_classes:
  data_read: true
  persistent_state_change: true
  outbound_network: true
  billable_compute: true
  sensitive_data_exposure: true
  notification_or_webhook: true
  idempotent: false

The tool can keep its friendly name. The policy engine should ignore that name when it decides whether the call needs approval.

Put an effect contract beside the tool schema

The JSON schema tells Claude Code which arguments a method accepts and which fields it returns. It rarely says what happens outside the response.

Add a versioned effect contract to the server definition:

method_effect_contract:
  server: reporting-mcp
  method: report.preview
  contract_version: 12
  effects:
    - type: billable_query
      unit: warehouse_bytes_scanned
      maximum_per_call: 5000000000
    - type: object_create
      target: report-preview-bucket
      maximum_per_call: 1
    - type: outbound_webhook
      destination_class: internal_reporting
      maximum_per_call: 1
    - type: audit_state_change
      transition: unseen_to_viewed
    - type: sensitive_data_read
      allowed_classes: [customer_account_id, payment_status]
  approval_required_when:
    estimated_cost_usd_gte: 1.00
    records_gte: 10000
    sensitive_data: true
    repeated_within_run_gte: 3
  default_call_limit_per_run: 5

Version the contract with the MCP server. If either the schema or implementation changes, expire the previous permission decision. Otherwise a safe approval can survive after the method has acquired a new side effect.

This contract also gives a reviewer something testable. “Read only” is an opinion. A maximum of one export and one webhook per call is a claim that an evaluator can check.

Estimate before execution and reconcile afterwards

Before Claude Code runs the method, ask the provider for a preflight estimate:

preflight:
  estimated_rows: 14000
  estimated_bytes_scanned: 8600000000
  estimated_cost_usd: 2.37
  creates_export: true
  sends_webhook: true
  approval_required: true

The estimate is not evidence that the call stayed inside its boundary. After execution, collect a receipt from the systems that performed the work:

observed_effects:
  warehouse_job_id: wh-example-4821
  rows_returned: 13742
  bytes_scanned: 8310000000
  cost_usd: 2.28
  export_object_id: exp-example-91
  webhook_event_id: evt-example-122
  audit_transition: unseen_to_viewed
  independently_verified: true

Do not let the MCP server grade its own behaviour. Reconcile the warehouse job with the warehouse provider, the object with storage inventory, and the webhook with the receiving service. If an observed effect is missing from the contract, quarantine the method until someone explains the mismatch.

Give every subagent the same run budget

Per-call thresholds will not stop a loop of individually acceptable calls. The 186 previews may differ by timestamp, pagination token, or harmless formatting while causing the same business effect.

Use one budget for the parent run and all child agents:

run_budget:
  warehouse_cost_usd: 8.00
  sensitive_records_read: 25000
  export_objects_created: 3
  outbound_webhooks: 3
  repeated_semantic_query_limit: 2

A subagent must debit the parent’s ledger. A new run ID is not a new allowance.

Normalize the requested outcome into a semantic effect key as well. For this example, the key might combine the tenant, reporting period, data class, and output type. The third attempt to generate the same customer payment preview should hit the repeated-effect limit even if Claude Code changes the wording or date format.

This is where cost control and permission control meet. The same loop that spends money can duplicate exports, widen data exposure, and annoy another service with repeated webhooks.

Test one undeclared effect

Build an eval where report.preview returns a normal result but emits an undeclared webhook. The fixture should fail even though the response is valid and the requested report is correct.

Add a second fixture where the estimated warehouse cost crosses the approval threshold. The query must not start before approval. Add a third where the server claims success but the provider receipt never appears. That outcome stays unresolved rather than becoming a silent success.

The release condition is specific:

release_gate:
  declared_effects_observed: 7
  undeclared_effects_observed: 0
  unresolved_provider_receipts: 0
  budget_breaches: 0
  approval_bypasses: 0
  result: pass

A tool that cannot produce this evidence should not receive unattended production access.

Put the receipt in the review packet

The final review packet should bind the contract version, approval, attempted calls, completed calls, blocked calls, cost, records read, external effects, and provider receipts to one run.

That is more useful than a transcript saying Claude Code used only read methods. A reviewer can see what the method caused, whether those effects were expected, and where the budget stopped repetition.

If your current permission catalogue has only read and write, start with the five MCP methods that touch production systems or paid services. Trace each method past the MCP response. Write the effect contract, add a preflight gate, and require a runtime receipt before calling any of them safe.

Claude Code: Building Production Agents That Actually Work contains the wider production model for MCP boundaries, permission budgets, observability, evals, rollback, and review packets.