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.
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.
-
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.
-
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.
-
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.
-
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.
-
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: inline viewer versus watch-link fallback
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.