Govern MCP Apps content
MCP Apps embed interactive UI components (charts, forms, dashboards) inside tool-call responses. Use this guide to:
- Strip UI blocks from untrusted upstreams while keeping text content
- Audit which upstreams emit UI blocks (without modifying anything)
- Block specific upstreams that abuse the UI surface
How it works
A tool-call response with an MCP-UI block looks like:
{
"jsonrpc": "2.0", "id": 1,
"result": {
"content": [
{"type": "text", "text": "Here's your dashboard"},
{"type": "ui", "mimeType": "application/vnd.mcp-ui+json", "data": {"url": "https://renderer.example/dashboard"}}
]
}
}
mcpgw recognises a UI block when either:
type == "ui", ormimeTypestarts withapplication/vnd.mcp-ui
When a strip_app rule matches a tools/call request, the gateway parses the response (whether returned as JSON or streamed via SSE), removes UI blocks from result.content[], and forwards the modified response. Text blocks and other content types are preserved.
Strip UI blocks from one upstream
policy:
rules:
- id: strip-untrusted-apps
action: strip_app
when:
tool_prefix: "untrusted_"
After SIGHUP, any tools/call to a tool starting with untrusted_ strips UI blocks before forwarding. Audit:
{"action":"strip_app","rule_id":"strip-untrusted-apps","tool":"untrusted_render","stripped":2,...}
Strip UI blocks from all upstreams
policy:
rules:
- id: strip-all-apps
action: strip_app
when: { tool_name: "*" }
The * wildcard matches every tool. Use this for tenants who have not opted into MCP Apps support yet — the surface is hidden entirely.
Allow apps from a specific upstream only
policy:
rules:
- id: allow-trusted-app-emitter
action: allow
when: { tool_prefix: "trusted_" }
- id: strip-everywhere-else
action: strip_app
when: { tool_name: "*" }
The first matching rule wins. Tools prefixed trusted_ allow UI through; everything else strips.
Monitor without modifying
mcpgw always counts UI blocks via the mcp.app.served telemetry attribute, even when no strip_app rule fires. Operators can dashboard the count to see which upstreams emit MCP Apps before deciding on policy.
To audit-only, configure no rule and tail telemetry:
# Span filter — find requests where served > 0
... | jq 'select(.["mcp.app.served"] > 0) | {tool, served: .["mcp.app.served"]}'
Verifying it works
Send a tools/call to an upstream that emits an MCP App. Watch:
tail -f /var/log/mcpgw/audit.jsonl | jq 'select(.action == "strip_app")'
Expected:
action=strip_app, stripped=N— N blocks removedmcp.app.stripped=Non the corresponding span- Response body contains text blocks but no
"type":"ui"orvnd.mcp-uimimeTypes
If the rule matches but no UI blocks were present, stripped=0 is recorded — useful for tracking rule effectiveness.
Interaction with other policy actions
When multiple rules apply to the same request:
- The first rule with
action: allow,deny,redact,rate_limit, orstrip_appwins. Order matters. - For SSE-streamed responses,
redact(per-frame) is evaluated independently from the inboundstrip_appdecision. If a frame matches a redact rule, redact runs and strip_app is skipped for that frame. Subsequent frames still get strip_app applied.
Limitations
JSON shape only. mcpgw recognises result.content[] arrays. Responses with content under a different shape (e.g. result.blocks[]) are not detected. As of this writing, MCP-UI uses result.content[].
No partial-block surgery. Either a block is identified as UI and removed wholesale, or it is preserved. mcpgw does not, for example, strip a URL out of a UI block while keeping the rest.
Body cap. Non-SSE responses are read up to 16 MiB. Larger responses are rejected with HTTP 502 and audit error_kind=upstream_too_large — they are NOT forwarded as-is. (Pre-Phase-5 byte-passthrough behavior is no longer available; if your upstreams legitimately return >16 MiB tool-call payloads, raise the cap or switch them to SSE.)
MimeType match is prefix-only. application/vnd.mcp-ui+json, application/vnd.mcp-ui+yaml, etc. all match. A bespoke mimeType like application/vnd.acme-ui+json would not — by design, since mcpgw only governs the documented MCP-UI surface.
Related
- Policy cookbook — strip_app recipe gallery
- Reference: policy rule schema
- Reference: audit log —
strip_appaction,strippedfield - Reference: telemetry —
mcp.app.served,mcp.app.strippedattributes - Reference: configuration —
strip_appin the policy action enum