The approval record looked precise:
SERVER deployment-mcp
TOOL release.inspect
TARGET staging
ACCESS READ
STATUS APPROVED
Claude Code called the approved tool. The client connected to mcp.tools.example, received an HTTP 307 redirect, and sent the request body to uploads.partner.example.
The bearer token was stripped before the second request. That helped. The tool arguments still contained a release ID, repository path, environment name, and incident reference. The approved server never received them. Another destination did.
Nothing in the tool approval caught the change because it bound permission to a logical server name. The network path was treated as plumbing.
A server name is not a network boundary
An MCP client may reach a server through DNS, a load balancer, service discovery, an outbound proxy, or a redirect. Each layer can change where bytes go without changing the tool name Claude Code sees.
TLS proves that the destination controls a certificate accepted for the hostname. It does not prove that this is the destination reviewed when the operator granted access. A valid certificate for a newly introduced host is still a different trust decision.
The same problem appears when a hostname starts resolving to a private address, a proxy rewrites the target, or service discovery moves a production tool onto a shared backend. Authentication can succeed while the approved boundary has changed.
This is why the MCP capability manifest needs transport identity as well as schema and implementation identity. The gateway should compare that identity immediately before dispatch, not only when the server is registered.
Bind approval to the transport
Store the connection facts that matter beside the tool capability:
mcp_transport_binding:
server: deployment-mcp
binding_version: 7
scheme: https
host: mcp.tools.example
port: 443
tls_spki_sha256:
- "sha256/example-current-key"
- "sha256/example-next-key"
allowed_ip_ranges:
- "203.0.113.0/28"
proxy_route: agent-egress-prod
redirects: deny
dns_max_age_seconds: 60
request_body_classes:
- repository_metadata
- deployment_metadata
expires_at: 2026-09-07T09:00:00Z
The public IP range above is documentation-only example space. In a real manifest, use the ranges your platform team owns and can verify.
Pinning only an IP address is brittle. Providers rotate addresses, and a shared edge may serve many tenants. Pinning only a certificate is also incomplete because certificate rotation is normal and one certificate may cover several hosts. Treat scheme, hostname, port, TLS identity, resolved network range, proxy route, and redirect policy as one binding.
Plan certificate rotation with an overlap window. Record the current and next public keys, approve the change through the same path as other boundary changes, then remove the old key. Do not turn off verification because a routine rotation caused an outage.
Deny redirects on authenticated tool calls
The safest default for an authenticated MCP endpoint is simple: do not follow redirects.
A redirect changes the destination after policy evaluation. The client should return a structured boundary error containing the original host, status code, proposed destination, and binding version. Claude Code can report the mismatch, but it should not decide that the new host is close enough.
{
"error": "mcp_transport_boundary_changed",
"approved_host": "mcp.tools.example",
"redirect_status": 307,
"proposed_host": "uploads.partner.example",
"binding_version": 7,
"request_sent": false
}
If a platform genuinely needs redirects, enumerate the permitted destination and re-run the transport gate at every hop. Set a small hop limit. Never forward credentials across origins by default, and remember that stripping the credential does not make the body harmless.
Relative redirects on the same origin still need a loop limit and path policy. A server that bounces a request between two paths can burn the run budget even when no data crosses an origin boundary.
Resolve, connect, then verify again
Checking DNS before a request is not enough if the client resolves the name again when it opens the socket. Resolve through the controlled egress layer, select an allowed address, connect to that address, and verify the actual peer used by the connection.
Record both the hostname and connected address:
transport_dispatch_receipt:
run_id: cc-run-5194
server: deployment-mcp
tool: release.inspect
binding_version: 7
requested_origin: https://mcp.tools.example:443
resolved_addresses:
- 203.0.113.6
- 203.0.113.7
connected_address: 203.0.113.6
tls_spki_sha256: "sha256/example-current-key"
proxy_route: agent-egress-prod
redirects_observed: 0
body_sent: true
boundary_result: pass
The egress proxy should produce or attest this receipt. Do not ask the MCP server to prove where the client connected. The destination cannot independently grade the route used to reach it.
For long-lived sessions, recheck the binding when the connection is created and whenever it reconnects. A connection pool must not silently reuse a session after the approval expires or the binding changes.
Test the route changes that happy-path tests miss
Build transport fixtures around the approved tool call:
fixtures:
- case: approved_host_and_tls_identity
expect: dispatch_allowed
- case: cross_origin_307_redirect
expect: blocked_before_body_send
- case: hostname_resolves_outside_allowed_range
expect: dispatch_blocked
- case: unplanned_certificate_key
expect: new_approval_required
- case: proxy_route_changes
expect: dispatch_blocked
- case: reconnect_after_binding_expiry
expect: new_approval_required
- case: same_origin_redirect_loop
expect: stopped_at_hop_limit
Check the failure evidence as carefully as the allow path. A blocked redirect should prove that no request body reached the proposed host. Proxy access logs, destination canaries, and packet metadata from a controlled test environment are stronger evidence than a client message saying request blocked.
Put the transport receipt in the review packet with the tool contract, permission decision, and observed effects. A reviewer can then answer two separate questions: was Claude Code allowed to call this tool, and did the call reach the endpoint that was approved?
Tool permission is incomplete until it covers the route. Bind approval to the transport, reject destination changes before sending the body, and make the actual connection part of the run evidence.
Claude Code: Building Production Agents That Actually Work contains the wider production model for MCP boundaries, permission budgets, observability, evals, rollback, and review packets.