Claude Code has approval to call release.create_draft. The MCP method accepts a service name, commit, and release notes. It creates a draft release record and cannot deploy anything.

A week later, the platform team ships a compatible-looking update. The method name stays the same. The new schema adds publish: boolean, defaulting to false, and the server identity still passes authentication. Another update changes the backend implementation so a draft for a production service also reserves a deployment slot.

The agent’s permission file has not changed. Its authority has.

A method name is a weak permission boundary. Before Claude Code uses a consequential MCP tool, I want the run to pin the exact contract it reviewed: server identity, schema, implementation, target scope, side-effect class, and policy version. If the live tool differs, the call stops.

MCP capability manifest verification flow

the method name does not describe the authority

Teams often write permissions like this:

allow:
  - mcp.release.create_draft

That line says which route the agent may call. It does not say what the route can do today.

The effective capability comes from several moving parts:

server identity
+ method schema and defaults
+ implementation and dependencies
+ credentials used by the server
+ target resource policy
+ side effects and downstream triggers
= actual authority

Any one of them can drift without changing the method name. A read method might start warming a cache that triggers billing. A ticket method might gain a default assignee with access implications. A deployment preview might begin writing to a shared environment.

This is why classifying tools by their effects matters. The permission should describe the action the system can cause, not the friendly verb in its API name.

issue a capability manifest at approval time

Capture the reviewed contract as an immutable manifest:

capability_manifest:
  id: cap-release-draft-7f38
  issued_for:
    run_id: cc-run-2941
    task_id: release-payments-2026-08-12
  server:
    identity: spiffe://platform/mcp/release
    version: 4.8.2
  method:
    name: release.create_draft
    schema_digest: sha256:542a...
    implementation_digest: sha256:a7c1...
  arguments:
    allowed:
      service: payments-api
      commit: 8bc911d
    forbidden:
      - publish
      - environment
  effects:
    class: reversible_external_write
    may_trigger:
      - draft_release_created
    must_not_trigger:
      - deployment_slot_reserved
      - production_rollout_started
  policy:
    version: release-policy-31
    expires_at: 2026-08-12T12:00:00Z

The schema digest should cover normalized argument types, required fields, defaults, and response shape. Defaults matter. Adding an optional flag can change behaviour even when old requests still validate.

The implementation digest can identify a signed server artifact, image, function package, or release attestation. Hashing source files from the client is usually the wrong layer. The runtime or deployment system should attest the artifact that is serving the request.

The effect list is the part most API contracts omit. Write it from observed and governed behaviour: records created, queues published, workflows started, credentials exercised, and systems reached. If the platform cannot name a tool’s possible effects, it is not ready to grant that tool to an autonomous run.

compare the manifest at dispatch

Do not check the manifest once when the session starts. Check it at the gateway immediately before every consequential call.

1. Claude Code submits method, arguments, and capability manifest ID.
2. Gateway fetches fresh server and deployment attestations.
3. Gateway normalizes the live schema and computes its digest.
4. Policy resolves the target and permitted effect class.
5. Gateway compares every bound field with the manifest.
6. Exact match allows dispatch. Any mismatch stops the call.

The agent should not perform this comparison itself. It can propose a call, but the gateway owns the decision. A prompt instruction such as “check that the schema has not changed” fails if the agent receives stale tool metadata or decides that a small difference looks harmless.

Here is a useful decision record:

capability_check:
  manifest_id: cap-release-draft-7f38
  checked_at: 2026-08-12T10:51:09Z
  live:
    server_identity: spiffe://platform/mcp/release
    server_version: 4.9.0
    schema_digest: sha256:8b20...
    implementation_digest: sha256:ce11...
    policy_version: release-policy-31
  differences:
    - field: method.schema_digest
      expected: sha256:542a...
      actual: sha256:8b20...
    - field: effects.may_trigger
      added: deployment_slot_reserved
  decision: deny_contract_drift
  dispatched: false

A version difference by itself need not be fatal if all bound properties remain provably identical. In practice, it is safer to bind the signed artifact digest and semantic contract rather than make the version string the whole control. The decision record should still preserve the version for investigation.

test drift as a denial path

A capability manifest is useful only if the runtime refuses near matches. Add denial fixtures before granting the tool:

denial_evals:
  - name: optional_publish_argument_added
    mutate: schema
    expect: deny_contract_drift
  - name: default_target_changes_to_production
    mutate: argument_default
    expect: deny_contract_drift
  - name: same_schema_new_side_effect
    mutate: implementation_attestation
    expect: deny_contract_drift
  - name: server_uses_wider_service_identity
    mutate: server_identity
    expect: deny_contract_drift
  - name: policy_changes_during_run
    mutate: policy_version
    expect: deny_contract_drift

Include one allow fixture for the exact approved contract. Then change one property at a time. This separates a working permission from a gateway that denies everything.

The awkward fixture is same_schema_new_side_effect. Schema checks cannot catch it. The implementation attestation and effect policy must. That is also why a green result against a mock MCP server does not prove the production tool stayed inside its approved boundary.

make renewal a new decision

When a check fails, do not silently refresh the manifest and continue. That turns attestation into paperwork.

Stop the call and return a contract delta to a named reviewer:

renewal_request:
  old_manifest: cap-release-draft-7f38
  requested_change:
    schema: adds optional publish flag
    effects: adds deployment_slot_reserved
  task_still_requires_tool: true
  safer_alternative:
    method: release.create_local_record
    effects: [local_file_write]
  required_decision:
    - reject_change
    - approve_new_manifest
    - choose_safer_alternative

Approval belongs to the new capability, not to the old method name. Issue a fresh manifest with a short expiry and bind it to the current run, task, arguments, and artifact. Record the reviewer and reason.

If the run can use a narrower path, prefer it. Producing a release file for human submission may be slower than creating the external record directly, but it preserves the original authority boundary while the tool change is reviewed.

This follows the same discipline as binding test evidence to the code it tested and rehearsing rollback against the current artifact. Evidence, recovery, and permission all expire when the thing they describe changes.

put the contract check in the review packet

The final review packet should identify both the approved and live capability:

mcp_use:
  manifest_id: cap-release-draft-7f38
  server_identity: spiffe://platform/mcp/release
  schema_digest: sha256:542a...
  implementation_digest: sha256:a7c1...
  policy_version: release-policy-31
  dispatches: 0
  denied_calls: 1
  denial_reason: contract_drift
  external_effects_verified: []

Zero dispatches can be the correct outcome. The agent found that yesterday’s permission no longer described today’s tool and refused to inherit the difference.

My rule is simple: approve a capability manifest, verify it at dispatch, and stop on any unreviewed delta. Claude Code should never gain new authority because an MCP method kept its old name.

Claude Code: Building Production Agents That Actually Work covers MCP boundaries, permission changes, denial evals, observability, rollback, and review packets for production coding-agent work.