Skip to main content
MCP Apps display an interactive view alongside an MCP tool result. This includes Browser MCP’s live browser viewer. Connecting an MCP server is not enough: the operator must also enable a separate sandbox listener and publish its per-connector HTTPS hosts. Text tool results work without it.

Choose isolated origins

Use a dedicated user-content domain on a different registrable site from the product. For example (replace both domains with ones you own): {server} is literal configuration, not a value you fill in yourself. The host replaces it with a stable 24-character lowercase hexadecimal hash of the MCP server registration ID. Different registrations get different origins. The runtime enforces all of these rules:
  • Exactly one {server} placeholder, in the first DNS label. The expanded label must fit the DNS limit of 63 characters.
  • A fixed registrable site below the placeholder. Do not use https://{server}.com or https://fixed.{server}.example-usercontent.net.
  • A different registrable site from BASE_URL, checked using the Public Suffix List including private suffixes. Changing the port or adding subdomains does not satisfy this rule.
  • An HTTPS origin, without credentials, a non-root path, query, or fragment. HTTP is allowed only for loopback development, not for public sandbox hosts.
chat.example.com and sandbox.example.com are not sufficiently isolated: both belong to example.com. Even https://mcp-apps-{server} .sandbox.example.com is rejected. Use an unrelated dedicated domain such as example-usercontent.net, not a shared domain that also hosts sensitive applications or sets parent-domain cookies.
Use the canonical product origin that users actually open. An alternate product hostname on the sandbox site will not work. BASE_URL, the origin template, and the existing SETTINGS_ENCRYPTION_KEY must agree between the product and sandbox.

Community Helm installation

Follow Install with Helm and the Quickstart secret setup first. The examples below use release r5d-chat, namespace r5d-chat, and an existing Secret r5d-chat-secrets. That Secret must contain DATABASE_URL, BETTER_AUTH_SECRET, SETTINGS_ENCRYPTION_KEY (at least 32 characters), POSTGRES_PASSWORD for the bundled database, and preferably SETUP_CLAIM_TOKEN. The database password in DATABASE_URL must match POSTGRES_PASSWORD. Choose a released chart/image pair that includes mcpApps.sandboxOrigin. Keep the public community image defaults; no managed image or operator-only workflow is needed. Select and pin a version rather than copying an old version number:

Values

Save this as values.yaml for a new installation. Replace domains and all three storage classes with your own. These are complete overrides on top of the chart defaults; secrets remain in the existing Secret. This example manages both TLS Ingress resources separately, so the chart’s product Ingress is off.
The peer above selects Traefik pods in kube-system (the two selectors in one peer are ANDed). Check your controller’s actual namespace and labels and adjust both. networkPolicy.controlPlaneIngressFrom is shared by product ingress on port 3000 and sandbox ingress on 3002. Preserve any other required product ingress/tunnel peers. An empty list allows any source on those ports; it does not mean deny-all. Enforcement requires a NetworkPolicy-capable CNI. For an existing installation, merge only the MCP Apps settings and necessary peer changes into your maintained release values. Do not replace existing storage, authentication, images, scheduling, or product routes with this sample. See Chart values for other installation options.

What is created

A nonempty mcpApps.sandboxOrigin makes the chart create:
  • Deployment r5d-chat-r5d-chat-mcp-apps, using the same community image as the product and command bun .output/automations/mcp-apps-sandbox.mjs.
  • ClusterIP Service r5d-chat-r5d-chat-mcp-apps, port 3002.
  • A sandbox NetworkPolicy when networkPolicy.enabled is true, with the ingress peers above and egress: []. Other policies must not grant it extra egress.
The sandbox receives only BASE_URL, MCP_APPS_SANDBOX_ORIGIN, and the settings key from the existing Secret. It receives no database/auth credentials, mounts no application storage, and does not mount a service-account token. The product also receives MCP_APPS_SANDBOX_ORIGIN automatically. The chart does not create a sandbox Ingress, wildcard DNS, or certificate. The usual ingress.* values configure only the product route. With a different release name, use the rendered <fullname>-mcp-apps Service name below.

Wildcard DNS, TLS, and Ingress

Create an A/AAAA record for chat.example.com and a wildcard A/AAAA or CNAME record for *.example-usercontent.net, pointing to your ingress endpoint. Only publish IPv6 if that endpoint works over IPv6. A wildcard covers one DNS label, which matches the generated mcp-apps-<hash> hosts. Obtain a certificate for chat.example.com and one for *.example-usercontent.net. For ACME wildcard issuance, use DNS-01 with your DNS provider (HTTP-01 cannot issue wildcard certificates). Provision these TLS Secrets in r5d-chat, or have your certificate controller create them:
Save the following as ingress.yaml. It uses standard Kubernetes Ingress with Traefik; adjust the class and controller-specific HTTPS listener settings to your installation. For an existing product, keep its current route instead of creating the first resource again.
Never send sandbox hosts to r5d-chat-r5d-chat:80 (the authenticated product). Do not configure a catch-all fallback from sandbox hosts to product routes. Preserve the public Host and /sandbox?t=... path/query through every hop; do not replace Host with a Service name or append the upstream port 3002.

External nginx TLS termination with HTTP upstream

HTTPS is required at the browser, not between a trusted TLS terminator and the sandbox. Keep the template https://mcp-apps-{server}.example-usercontent.net even when nginx uses an HTTP upstream. The listener derives the scheme from that configured template and validates the incoming public Host; it deliberately ignores X-Forwarded-Host and X-Forwarded-Proto as authority substitutes. For example, nginx outside Kubernetes can terminate wildcard TLS and forward to a private HTTP ingress endpoint. In this topology, replace only the sandbox Ingress above with this sandbox-http-ingress.yaml (no TLS section):
That internal web entrypoint must accept HTTP without an HTTPS redirect. Use a dedicated private entrypoint if your normal HTTP entrypoint redirects; do not disable HTTPS redirects on unrelated public routes. Restrict access to the internal endpoint to your proxy. Save this server block in nginx’s http context, replacing the private endpoint and certificate paths:
Keep the product virtual host separate and unchanged. Inspect inherited nginx, CDN, and ingress configuration too: do not inject scripts or override the sandbox’s security headers. Run nginx -t before reloading your configuration. If using a non-default public HTTPS port, include it in the template and retain it in Host; never substitute the internal upstream port. With the HTTP ingress hop, the sandbox normally sees ingress-controller pods, not nginx, as the network source. If nginx can reach the sandbox Service directly on the cluster network, you can instead proxy to that Service on port 3002 and omit the sandbox Ingress. In that case allow nginx’s actual pod or observed source IP using networkPolicy.controlPlaneIngressFrom (for example a narrowly scoped ipBlock), accounting for your CNI’s SNAT behavior. Do not disable all NetworkPolicy to fix a timeout. A tunnel connector likewise needs its own peer and must preserve the public Host while targeting the sandbox Service.

Why sandbox routes must stay bare

The sandbox serves signed, 15-minute policy links, not a second authenticated copy of the product. Authentication and current MCP authorization are checked by the product’s /api/mcp-apps endpoint before it supplies the iframe content.
  • No cookies or auth middleware: SSO/login redirects break the iframe handshake and put privileged pages or credentials on an untrusted-content route. Do not share product session cookies with it.
  • No HTML injection or rewriting: analytics, CDN optimizers, banners, and script rewriting can break the message bridge or introduce code outside the resource’s declared policy.
  • No caching: preserve Cache-Control: no-store. Do not cache signed links or their responses, even if a CDN normally ignores query strings. Avoid recording full signed URLs in access logs, screenshots, or support reports.
  • Preserve security headers: leave CSP, Permissions-Policy, and referrer policy intact. Do not add X-Frame-Options: DENY/SAMEORIGIN or a generic CSP that blocks the intended cross-site iframe.
Declared App network destinations may receive data the App sends. The host filters out product and sandbox sites from those declarations; it cannot make an untrusted server trustworthy. An unrelated domain dedicated to this content reduces exposure to other applications’ cookies and same-site privileges. See Security.

Apply and verify

Render using the same pinned version and reviewed values that you will install. Helm rendering checks Kubernetes configuration, not the runtime origin rules above. Keep rendered output private, especially if you do not use an existing Secret.
For external nginx, apply sandbox-http-ingress.yaml instead of the TLS sandbox Ingress; keep your existing product routing. The sandbox validates its origin configuration before listening, but its readiness probe is TCP only. A Ready pod does not prove that the product has matching configuration or that public DNS, TLS, routing, or App authorization works.
  1. Check DNS and TLS for a generated-shaped host without bypassing certificate checks. This request should return 403 because it has no signed ticket:
    Expect Cache-Control: no-store, no Set-Cookie, and no login redirect. This is only a routing smoke test: an invalid sandbox configuration can also return 403. The sandbox has no /api/health endpoint.
  2. Sign in at the canonical product URL. Add/test/trust an App-capable MCP server in Settings → MCP, select it in the chat, and request a fresh App-bound tool call (for Browser MCP, browser_watch). Old results without a recorded App binding remain text-only.
  3. Click Open interactive view below the assistant’s work. No iframe or App resource request is made until you open it. The saved tool result is delivered without rerunning the original tool call.
  4. In browser developer tools, confirm /api/mcp-apps succeeds and the iframe’s /sandbox?t=... loads with 200 from a generated host on the dedicated site. Do not copy its signed URL into support reports. View connected means the handshake and result delivery succeeded, not that live browser frames arrived.
  5. Close/reopen the view, check the original text result is still readable, and verify current authorization: deselecting/disabling the MCP server must deny subsequent App requests. A different connector must use a different hostname.
Browser MCP must already have a connected, approved native CLI browser and a current MCP credential. Live pixels require a separate local opt-in on the browser host; enabling this Helm feature does not turn them on:
Use your Browser MCP server URL, not the chat or sandbox URL. If that profile is already running, stop it deliberately and restart the same profile with --live-view; do not delete its profile or credentials. See the current Browser MCP native CLI guide and live-view guide. browser_watch supplies an MCP App plus a dashboard watch link. With this sandbox configured, open the inline viewer. Without a working inline viewer, use the link from the tool’s text result and sign in to the Browser MCP dashboard; it is not an anonymous stream or a sandbox URL. Both modes still require the native runtime’s local live-view opt-in. Frames never enter tool results and may show sensitive, unmasked page content. The inline App uses the app-only browser_watch_ticket tool for short-lived view-only tickets. r5d.chat prompts for every App-initiated tool call, including ticket renewal or reconnection; approve only the expected requests. Declining can leave the viewer waiting without making the underlying browser offline.

Troubleshooting

The UI intentionally says Interactive view unavailable rather than exposing upstream details. Product-side origin validation failures become the generic MCP App request failed (502) response from /api/mcp-apps. The standalone sandbox validates configuration on startup; a failure prevents it from listening and can cause CrashLoopBackOff. Check its container logs for the validation messages below. Rejected requests to a running listener fail closed with 403; these validation messages are not guaranteed browser-visible error text.

Rollback without resetting data

To disable inline views, set this in your existing full release values and perform a reviewed Helm upgrade with the same chart/image versions:
The chart removes its sandbox Deployment/Service/NetworkPolicy and clears the product template. Remove only the separately managed sandbox Ingress/nginx route and, if desired, its dedicated DNS/TLS resources. Keep the product route and other consumers of any shared certificate intact. Existing text results and Browser MCP’s dashboard watch-link fallback remain available. No database reset, PVC deletion, reinstall, or encryption-key rotation is required. Do not change SETTINGS_ENCRYPTION_KEY to troubleshoot: it protects stored credentials and App bindings. Enabling/disabling this setting needs no feature-specific data migration; ordinary release upgrades may still run their normal migrations. See Backup and restore before any unrelated data operation.

Local development

Supply these settings and the same existing settings key to both processes:
Run bun run dev and, separately, bun scripts/mcp-apps-sandbox.ts. Open the product at 127.0.0.1, not localhost (which shares the sandbox’s site). MCP_APPS_SANDBOX_HOST and MCP_APPS_SANDBOX_PORT override only the standalone listener’s default 0.0.0.0:3002; they do not change the public origin template or the Helm Service port. See Local development.