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", or
  • mimeType starts with application/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 removed
  • mcp.app.stripped=N on the corresponding span
  • Response body contains text blocks but no "type":"ui" or vnd.mcp-ui mimeTypes

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, or strip_app wins. Order matters.
  • For SSE-streamed responses, redact (per-frame) is evaluated independently from the inbound strip_app decision. 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.