MCP Tool Drift Detection (Exchange)
A Mulesoft Flex / Omni Gateway custom policy that detects MCP tool drift against the descriptor set published to an Anypoint Exchange MCP asset — and (optionally) strips the drifted tools from the response before they reach the LLM client.
The Exchange asset is the contract. The gateway is the enforcement point. Decisions are local; evidence flows into the customer’s existing Anypoint analytics pipeline. No external SaaS, no data leaving the tenant.
What it catches
- Descriptor drift — runtime descriptor hash ≠ published hash.
Reported as
description_changed,input_schema_changed,output_schema_changed, orannotation_changed. - Unpinned tools — present at runtime, absent from the Exchange asset.
- Removed tools — present in the asset, absent at runtime.
- Version changes — when
exchange.version=latest, a change in the underlying version between refreshes emitsversion_changed.
How it works
- Bootstrap — on policy attach, the Exchange client fetches the
pinned descriptor set from
{baseUrl}/exchange/api/v2/assets/{groupId}/{assetId}/{version}/mcp.json, authenticating with OAuth2 Connected App client credentials or basic auth. - Pin construction — each tool descriptor is canonicalized
(key-sorted JSON over
name,description,inputSchema,outputSchema,annotations) and hashed (SHA-256). - Refresh loop — a timer refreshes the pin every
refreshIntervalSec(default 300s). On failure, the last-known-good pin is retained. - Response interception — every
tools/listresponse is parsed, and each tool’s canonical hash is compared to the pin. - Enforcement — in
enforcemode, offending tools are stripped from the forwarded response before the agent sees them.
Modes
enforce— strip drifted tools fromtools/listresponses.warn— pass through with anx-mcp-drift-warningheader.observe— emit structured evidence only.
Configuration
| Path | Type | Default | Notes |
|---|---|---|---|
exchange.orgId | string | required | Anypoint business group. |
exchange.groupId | string | required | Exchange group id. |
exchange.assetId | string | required | Exchange asset id (the MCP asset). |
exchange.version | string | required | Pinned semver or latest. |
exchange.baseUrl | string | https://anypoint.mulesoft.com | Override for sovereign / EU. |
exchange.authType | enum | oauth2_client_credentials | or basic. |
exchange.credSecretRef | string | required | Flex secret holding the credentials. |
exchange.refreshIntervalSec | int 30–86400 | 300 | Refresh cadence. |
enforce.exactMatch | bool | true | Strict hash equality. |
enforce.allowAddedTools | bool | false | Unpinned tools blocked by default. |
enforce.allowRemovedTools | bool | true | Deprecation allowed. |
mode | enum | enforce | enforce / observe / warn. |
failOpen.onPinUnavailable | bool | false | Allow traffic on bootstrap when LKG is empty. |
Evidence
Every decision lands as a JSON log line through the PDK logger. Anypoint Analytics indexes these automatically; they never leave the customer’s tenant.
{
"class": "descriptor_drift",
"severity": "critical",
"decision": "stripped",
"asset_id": "demo-mcp-asset",
"asset_version": "1.4.2",
"tool_name": "lookup_account",
"pin_hash": "ab12...",
"runtime_hash": "cd34...",
"field": "description_changed"
}class ∈ descriptor_drift | unpinned_tool | removed_tool | version_changed | pin_stale | pin_unavailable.
Failure modes
- Exchange unreachable on bootstrap.
failOpen.onPinUnavailablecontrols allow/block. Default is closed (block +pin_unavailableevidence). - Exchange unreachable after bootstrap. Last-known-good cache is
retained;
pin_staleevidence fires at every failed refresh. - Asset version moved. When
version=latest, the underlying resolution change emitsversion_changed. Pin a semver to opt out.
Exchange asset format
The policy expects an Anypoint Exchange asset of type mcp with the
standard JSON-RPC tool envelope:
{
"tools": [
{
"name": "lookup_account",
"description": "Look up an account by its identifier.",
"inputSchema": { "type": "object", "properties": { } },
"outputSchema": { },
"annotations": { }
}
]
}This is the format produced by anypoint-cli-v4 exchange asset upload --type mcp.
Reference deployment
| Field | Value |
|---|---|
| Gateway | agent-network-ingress-gw (id 35755bec-3177-4d32-a8c9-c9705f5b1c0b, gw 1.13.2) |
| Public URL | https://agent-network-ingress-gw-zovwbn.jeg62f.usa-e2.cloudhub.io/mcp-drift-via-exchange-demo |
| API instance | 20999090 |
| Exchange asset (pin source) | 82a0453b-22e6-430d-bbf4-35b989d043dc/drift-demo-a2d-mcp/1.0.0 |
| Upstream (A²D mock) | https://www.a2d-ai.com/api/platform/7b26e0d0-dfcf-4c6a-8484-8c907724366d/mcp |
| Policy version | omni-policy-mcp-tool-drift-via-exchange-dev/0.1.0-20260629203732 |
The Exchange asset and the runtime upstream are derived from the same
A²D MCP server, so a healthy tools/list matches the pinned set
exactly.
curl -sS -X POST \
https://agent-network-ingress-gw-zovwbn.jeg62f.usa-e2.cloudhub.io/mcp-drift-via-exchange-demo \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'FAQ
Why an Exchange-only variant? Some customers run entirely on Anypoint and want a single source of truth they already understand — the Exchange asset. No external dependencies, no remote calls on the request path, no extra signing keys to rotate.
What if I want real-time policy decisions instead of a local cache? That is the role of the platform-managed drift detection (via A²D) variant.
Pair with
- MCP Tool Drift Detection (via A²D) — when A²D should be the source of truth.
- MCP Tool Poisoning Detection (Exchange) — adds shadowing + prompt-injection heuristics on top of the same Exchange pin.
Next Steps
Exchange-anchored drift detection keeps every decision inside the tenant.