Claude Code prepares a deployment inspection and shows the reviewer this request:

tool: release.inspect
service: payments-api
target: staging
include_logs: true

The reviewer approves it. The MCP client then translates target into the server’s environment field. A compatibility adapter drops the value because it only recognizes stage, not staging. The server fills the missing field with its default:

environment: production

The request is valid according to the schema. The permission check passed. The tool did exactly what its executable payload asked. It still crossed the boundary the reviewer thought they had set.

MCP argument binding receipt for Claude Code

The friendly request is not the executable request

Tool requests often pass through more than one representation:

  1. Claude Code produces model arguments.
  2. A client maps names and types into the MCP schema.
  3. An adapter expands aliases and compatibility fields.
  4. The server applies defaults.
  5. A backend resolves names into resource identifiers.

Any step can change the effective action. A blank environment can become production. all: false can disappear during serialization and return as a server default. A service alias can resolve to a production account even though the displayed name looks harmless.

Approving the first representation does not control the fifth one.

This is different from a drifting tool contract. Pinning the MCP schema and implementation tells you whether the tool changed. Argument binding tells you whether this particular request changed while travelling through an otherwise approved tool.

Normalize before the policy decision

Move aliases, defaults, coercion, and resource resolution in front of authorization. Produce one canonical request that contains no implicit values.

canonical_tool_request:
  request_id: req-7c41
  run_id: cc-run-8296
  tool: release.inspect
  tool_contract_version: 18
  service_input: payments-api
  service_id: svc-0042
  target_input: staging
  environment: production
  environment_source: server_default
  include_logs: true
  log_scope: deployment_summary
  adapter_version: compat-6
  normalization_status: boundary_changed

The environment_source field matters. A production value typed explicitly and a production value supplied by a default may execute the same query, but they carry different review evidence. Hidden defaults should never inherit approval from nearby prose.

Reject unknown aliases rather than deleting them. If the adapter does not understand staging, return a structured error. Silent omission turns a spelling or compatibility problem into a permission problem.

{
  "error": "argument_normalization_changed_boundary",
  "field": "environment",
  "input": "staging",
  "normalized": "production",
  "source": "server_default",
  "dispatched": false
}

The model can repair the request, but it cannot waive the mismatch.

Bind approval to a digest

Show the reviewer the canonical request, then hash that exact representation. The policy engine and dispatch layer should verify the same digest.

approval_binding:
  approval_id: approval-ap-3118
  canonical_request_sha256: "sha256:6f31..."
  resource_ids:
    - svc-0042
  environment: staging
  allowed_effects:
    - read_deployment_summary
  expires_at: 2026-09-02T11:00:00+01:00

In this case, the normalized request says production while the approval says staging. Dispatch must stop. Do not ask whether the two requests are semantically close. Environment is part of the authority boundary.

Canonicalization needs a published rule. Sort object keys, preserve arrays where order has meaning, use one representation for numbers and booleans, reject duplicate keys, and identify the schema version. Otherwise, two components can hash different bytes while believing they approved the same action.

Do not trust a digest supplied by Claude Code or the MCP server. The gateway should build the canonical payload and compute the digest from the request it is about to send.

Record the values that appeared from nowhere

A review packet needs more than the final JSON. It should explain how every consequential field acquired its value.

argument_binding_receipt:
  run_id: cc-run-8296
  tool_call_id: call-44
  tool: release.inspect
  contract_version: 18
  adapter_version: compat-6
  input_digest: "sha256:90a2..."
  canonical_digest: "sha256:6f31..."
  transformations:
    - field: target
      input: staging
      action: dropped_unknown_alias
    - field: environment
      input: null
      output: production
      action: applied_server_default
  approved_environment: staging
  executable_environment: production
  boundary_match: false
  dispatched: false
  status: blocked

This receipt gives a reviewer a usable answer. The call was blocked because normalization changed the environment, not because the model suddenly chose production. That distinction points to the adapter defect and makes the failure reproducible.

Keep the receipt at the gateway. Claude Code can summarize it, but the component that observes the final request must own the evidence. The same separation is needed when a transport redirect changes which MCP server receives the request.

Test defaults as permission changes

Build eval fixtures for transformations that ordinary schema validation accepts:

fixtures:
  - case: explicit_staging_environment
    expect: dispatch_allowed
  - case: unknown_target_alias_is_dropped
    expect: normalization_error
  - case: omitted_environment_defaults_to_production
    expect: fresh_approval_required
  - case: string_false_coerces_to_boolean_true
    expect: normalization_error
  - case: service_alias_resolves_to_another_account
    expect: boundary_mismatch
  - case: adapter_changes_after_approval
    expect: digest_mismatch
  - case: canonical_payload_changes_during_retry
    expect: fresh_approval_required

Assert that the production backend received zero requests for each blocked case. A client-side error is weak evidence if the request might already have crossed the gateway.

Retries need the same check. Rebuild and reauthorize the canonical request if service discovery, adapter version, tool contract, or resolved resource changes. Reusing the old approval because the model arguments look identical misses the part that actually changed.

I would not approve a production MCP call from a friendly summary alone. Show me the fully resolved payload, bind the decision to it, and give me a receipt when defaults or aliases alter the boundary.

Claude Code: Building Production Agents That Actually Scale covers the wider operating model for MCP boundaries, permissions, evals, observability, rollback, cost control, and review packets.