MCP Tool Poisoning Detection (Exchange)
A Flex Gateway / Omni Gateway custom policy that protects agentic
workloads against MCP tool poisoning, descriptor drift, and
name-shadowing attacks by pinning the trusted tool set in Anypoint
Exchange and enforcing that runtime MCP tools/list responses match
the published contract.
The detection logic mirrors the A²D variant; only the pin source differs. Use this variant when you run entirely inside Anypoint and want zero external dependencies.
Three classes of attack
- Tool poisoning — adversarial instructions injected into a
tool’s
description,inputSchema, or annotations that instruct the LLM to exfiltrate data, bypass safety filters, or chain into unintended tools. - Descriptor drift / rug-pull — a tool that passed review is silently modified post-publication, causing agents to act on altered instructions without operator awareness.
- Tool shadowing — a malicious tool with a near-identical name to
a trusted one (
get_uservsget_userr) is added to siphon agent calls.
The policy intercepts every MCP tools/list response, compares the
runtime descriptors to a pinned set stored in Anypoint Exchange, and
(in enforce mode) strips any tool that fails validation before the
agent sees it.
Architecture
┌──────────────┐ ┌────────────────┐ ┌─────────────────┐
│ │ │ │ │ │
│ MCP Server │──────▶│ Flex Gateway │──────▶│ Agent / Client │
│ │ list │ + Policy │ list' │ │
└──────────────┘ └────────┬───────┘ └─────────────────┘
│
│ fetch pin
│ emit evidence
▼
┌────────────────┐
│ Anypoint │
│ Exchange + │
│ Analytics │
└────────────────┘The policy never sends data outside the customer’s Anypoint tenant. Exchange fetch, pin storage, and evidence emission are all in-tenant operations.
Detection model
The policy runs five independent signal families against every
tools/list response.
Descriptor drift
Runtime canonical hash ≠ pinned hash. Canonical hash = SHA-256 over a
key-sorted JSON form of name, description, inputSchema,
outputSchema, annotations. The policy reports which field
changed — description_changed, input_schema_changed,
output_schema_changed, annotation_changed, descriptor_changed.
Severity: critical. Decision: strip in enforce, log in observe,
annotate in warn.
Unpinned tool
Tool present at runtime, absent from the pin. By default
(enforce.allowAddedTools=false), unpinned tools are stripped in
enforce mode. If allowAddedTools=true, they pass through — useful
during staged rollout of new tools before the pin is updated.
Severity: warning.
Removed tool
Tool present in the pin, absent at runtime. Informational by default
(enforce.allowRemovedTools=true) — normal deprecation workflow.
Severity: info.
Tool shadowing
Unpinned tool name within shadowingDetection.maxEditDistance
(Levenshtein) of any pinned tool name. For example, pinned get_user
- runtime
get_userr→ distance 1 → flagged.
Severity: critical. Decision: strip in enforce.
Prompt-injection heuristics
Built-in or custom regex matches anywhere in descriptor text fields. Built-in patterns include “ignore previous instructions” variants, exfiltration phrases, base64/hex blobs, and zero-width Unicode. See the A²D variant docs for the full pattern list.
Severity: warning. Heuristics alone do not block — informational by
design. Future releases will combine signals (unpinned + heuristic →
strip).
Configuration
Required
| Field | Description |
|---|---|
exchange.orgId | Anypoint business group that owns the MCP asset. |
exchange.groupId | Exchange group ID (typically the org ID). |
exchange.assetId | Exchange asset ID (the MCP asset). |
exchange.version | Pinned semver or latest. |
exchange.credSecretRef | Flex secret entry containing {clientId,clientSecret} or {username,password}. |
Optional
| Field | Default | Description |
|---|---|---|
exchange.baseUrl | https://anypoint.mulesoft.com | Control-plane URL. Use https://eu1.anypoint.mulesoft.com for EU sovereignty. |
exchange.authType | oauth2_client_credentials | or basic. |
exchange.refreshIntervalSec | 300 | Refresh cadence (30–86400). |
enforce.exactMatch | true | Hash equality. |
enforce.allowAddedTools | false | Pass unpinned tools. |
enforce.allowRemovedTools | true | Allow pinned tools to be removed. |
heuristics.enabled | true | Run heuristic detectors. |
heuristics.patterns | [] | Operator-supplied regex (validated at attach). |
shadowingDetection.enabled | true | Detect near-name twins. |
shadowingDetection.maxEditDistance | 2 | Levenshtein threshold (1–10). |
mode | enforce | enforce / observe / warn. |
failOpen.onPinUnavailable | false | Allow traffic on bootstrap when LKG is empty. |
Evidence
Every decision produces a JSON event emitted via the PDK logger. Anypoint Analytics indexes them automatically.
{
"class": "descriptor_drift",
"severity": "critical",
"decision": "stripped",
"asset_id": "poisoning-demo-a2d-mcp",
"asset_version": "1.0.0",
"tool_name": "assist_user",
"pin_hash": "ab12...",
"runtime_hash": "cd34...",
"field": "description_changed",
"patterns": ["(?i)\\bignore previous instructions\\b"],
"shadowed_target": null
}All events are prefixed with mcp-poisoning-evt. Query in Anypoint
Analytics with:
"mcp-poisoning-evt" AND severity:criticalDecision modes
enforce(default) — strip drifted/poisoned/shadowing tools.tools/callfor stripped tools is denied with JSON-RPC error code-32021 (tool_poisoned)or-32022 (tool_unpinned).observe— pass through unchanged; emit evidence withdecision: allowed. Use during onboarding to measure false-positive rate.warn— pass through with anx-mcp-poisoning-warningheader listing detected classes (comma-separated). Downstream clients can render a warning banner without blocking the agent.
Failure modes
| Condition | Behavior |
|---|---|
| Exchange unreachable on bootstrap, no LKG | If failOpen.onPinUnavailable=true: allow; else deny. pin_unavailable event emitted. |
| Exchange unreachable mid-flight | Retain LKG; emit pin_stale event when age > 2× refresh. |
| OAuth token mint fails | Same as unreachable — LKG preferred. |
version=latest and underlying version bumps | Fetch new pin, emit version_changed info event. Old LKG dropped after successful refresh. |
| Invalid heuristic pattern in config | Policy fails to attach with descriptive error. |
| Runtime body is not valid JSON-RPC | Pass through untouched. |
| SSE streaming response | Pass through untouched, log warning. SSE rewrite is roadmap. |
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-poisoning-via-exchange-demo |
| API instance | 20999092 |
| Exchange asset (pin) | 82a0453b-22e6-430d-bbf4-35b989d043dc/poisoning-demo-a2d-mcp/1.0.0 |
| Upstream (A²D mock) | https://www.a2d-ai.com/api/platform/6886e506-0b3b-4641-b8e0-037aad5fc240/mcp |
| Policy version | omni-policy-mcp-tool-poisoning-detection-exchange-dev/0.1.0-20260629203315 |
The Exchange asset was published from the same A²D mock that backs the
upstream, so a clean tools/list matches the pin. The mock declares a
benign fetch_weather and a deliberately poisoned assist_user whose
description contains a prompt-injection marker.
curl -sS -X POST \
https://agent-network-ingress-gw-zovwbn.jeg62f.usa-e2.cloudhub.io/mcp-poisoning-via-exchange-demo \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'FAQ
Why pin via Exchange instead of a ConfigMap / CRD / sidecar? Because Anypoint customers already publish MCP assets to Exchange as part of their API lifecycle (Design Center → Exchange → API Manager). Reusing that artifact means the policy inherits the customer’s existing governance — approval workflows, version control, audit logs, and rollback discipline — for free.
Does the policy block tools/call for stripped tools?
Not yet — the current release strips tools from tools/list but does
not add a request filter to gate tools/call. The agent typically
won’t call a tool it didn’t see in the list, so this is
defense-in-depth rather than a critical gap.
Does this need any data to leave the tenant? No. The pin is fetched from the customer’s own Exchange instance (in-tenant), every decision happens in-policy (Flex Gateway WASM sandbox), and evidence events are emitted to Anypoint Analytics (in-tenant). There is no external callback, no SaaS dependency, and no cross-tenant data movement.
Pair with
- Poisoning Detection (A²D) — same detection, A²D-anchored pin with approver attribution.
- Drift Detection (Exchange) — same pin source, drift-only scope.
Next Steps
Exchange-anchored poisoning detection keeps the trust contract inside your tenant.