Description
Allow configuring the advertised MCP Apps sandbox URL (e.g. MCP_SANDBOX_FULL_ADDRESS)
Summary
Please add a way to override the public MCP Apps sandbox URL returned by GET /api/config as sandboxUrl, independent of the process bind address.
Today the sandbox listen URL is built in clients/web/server/sandbox-controller.ts roughly as:
sandboxUrl = `http://${urlHost}:${actualPort}/sandbox`;
When the server binds 0.0.0.0, urlHost is forced to localhost. There is MCP_SANDBOX_PORT for the port, but no env/config for scheme + hostname (unlike v1’s MCP_PROXY_FULL_ADDRESS for the proxy).
Why this matters
In remote / container / lab setups the browser does not share the Inspector process’s localhost:
- UI is reached at a public hostname, e.g.
https://inspector.example.com/ - Sandbox is (or can be) routed separately, e.g.
https://inspector-sandbox.example.com/sandbox /api/configstill advertiseshttp://localhost:6275/sandbox- “Open App” points the iframe at the browser’s localhost → connection refused / mixed content on HTTPS
Routing port 6275 and setting ALLOWED_ORIGINS is not enough; the advertised sandboxUrl must be a URL the browser can actually open.
This also affects Docker, k8s ingress, and any reverse-proxy hostname split between UI and sandbox.
Proposed solution
Something parallel to v1 MCP_PROXY_FULL_ADDRESS, for example:
MCP_SANDBOX_FULL_ADDRESS=https://inspector-sandbox.example.com/sandbox
Behavior:
- If set, use it as
sandboxUrlinGET /api/config(and banner). - If unset, keep current bind-derived
http://<host>:<port>/sandboxbehavior. - Document that the process must still listen on
MCP_SANDBOX_PORT(or equivalent) and that the public URL must be routed to that listener. ALLOWED_ORIGINSremains responsible for UI origin /frame-ancestors(orthogonal).
Alternatives considered
- SSH/port-forward both 6274 and 6275 so the browser uses localhost (works for dev; not for public Instruqt/k8s hostnames).
- Rewrite
/api/configat the gateway (works as a lab hack; shouldn’t be required). - Patch
sandbox-controller.tslocally.
Related
- Docs already note forwarding both UI and sandbox ports for remote App review.
- #1276 is related remote/embed friction (referrer allowlist), but not the advertised URL override.
- v1 precedent:
MCP_PROXY_FULL_ADDRESS.
Environment
@modelcontextprotocol/inspectorv2.0.0- Instruqt / k3d: UI on public HTTPS hostname; sandbox needs a second public hostname → pod
:6275
Activity
eitansuez commentedon Jul 31, 2026
Local implementation (lab-verified)
In this branch is an implementation for this issue, which I verified with an instruqt lab that launches an mcp inspector tab connecting to the example mcp server system-monitor-server from the mcp-apps extension.
there were three distinct parts:
sharing intent + verification rather than a PR.
Behavior before
/api/configadvertisedhttp://localhost:6275/sandboxunder0.0.0.0bindBehavior after
MCP_SANDBOX_FULL_ADDRESS— public sandbox URL in/api/config(listen port unchanged; pin withMCP_SANDBOX_PORT)clients/web/staticin the npm packlist (otherwise Docker/npm ENOENT on/sandbox)ALLOWED_ORIGINSintosandbox_proxy.htmlreferrer check (was localhost-only)Env that worked in lab
MCP_SANDBOX_PORT=6275MCP_SANDBOX_FULL_ADDRESS=https://<sandbox-host>/sandbox# path requiredALLOWED_ORIGINS=https://<inspector-ui-host># origin only, no pathVerification
cliffhall commentedon Aug 1, 2026
Triage: Priority = Low
Scored 5/16 with the priority rubric added to
AGENTS.mdin #1891, as part of the backlog triage in #1872. Added to project board #28 with Status Incoming — meaning filed and scored but not yet approved for work. A maintainer decides whether it moves to Todo.Related issues. One connected issue:
Scores are a starting point, not a verdict — the rubric is meant to be overruled when it's plainly wrong, provided the reason is written down.
cliffhall commentedon Aug 29, 2026
Assessment: implementable, but harder to get right than it looks — and the premise needs checking first
I reviewed this against the code it touches (
clients/web/server/sandbox-controller.ts,clients/web/static/sandbox_proxy.html,clients/web/server/app-origin-controller.ts,clients/web/server/web-server-config.ts) and then went and verified the Docker claim empirically rather than reasoning about it.Short version: the advertised-URL override is not dangerous in itself — it's a string in
/api/config. But one of the three parts touches a real security boundary, and the whole thing sits on top of the MCP Apps sandbox, which is the most delicately-guarded surface in the client and the one we least want to break. Meanwhile the motivating premise — that this is needed for containerized deployment — does not hold: MCP Apps works in Docker today, verified below.First: does it work in Docker right now?
Yes. Verified, not assumed.
Built the image from the current
v2/mainDockerfile, ran it with the port publishing the README documents, and drove the real connect → open app →data-app-status="ready"chain in headless Chromium against the container, reusingscripts/lib/mcp-app-flow.mjsso the assertion is the same contractsmoke:web:appuses.Phase 1 — default (opaque-origin) render,
mcp-app-http.json: PASSPhase 2 — dedicated origin (
_meta.ui.domain, #2056),mcp-app-domain-http.json: PASSThat is exactly the intended isolation model: host, sandbox proxy, and app document each on a distinct origin, with the default render's inner document at the opaque
nullorigin. The banner advertisedhttp://localhost:6275/sandboxand the browser reached it.So the container case — the one deployment scenario we actually ship and support — is not broken, and is not an argument for this change.
And a prior blocker sitting in front of the remote case
While testing whether a non-loopback hostname works, I hit something upstream of the sandbox entirely. Browsing the same container at
http://inspector.localtest.me:6274(a public DNS name that resolves to127.0.0.1, so a genuinely non-loopback origin pointed at the same server), the connect fails before any App is involved:crypto.randomUUIDonly exists in a secure context — HTTPS, or alocalhost/127.0.0.1origin.core/mcp/inspectorClient.tscalls it unguarded intrackRequest(and several other places; note line 692 already carries a?? Math.random()fallback, so the inconsistency is visible in-file). The result is that the web client is dead over plain HTTP on any non-localhost host — a LAN IP, a container published on a routable address, a plain-HTTP reverse proxy — for reasons that have nothing to do with the sandbox.That matters for this issue in two ways: it means the remote story is broader than an advertised URL, and it means anyone reaching for
MCP_SANDBOX_FULL_ADDRESSover plain HTTP will hit a different wall first.If we do implement it, these are the things to design in
1. Same-origin collapse — the one genuine security risk.
The MCP Apps spec requires host ≠ sandbox origin. Today that is structural: the sandbox is on its own port, bound by the same process, so it cannot be misconfigured. An operator-supplied full address makes it configurable — and the most natural reverse-proxy setup,
https://inspector.example.com/sandbox→:6275, is precisely the one that collapses it.Two backstops do fail closed: the proxy's
window.top?.alert()self-test throws "The sandbox is not setup securely", and the #2056srccheck refuses an app origin equal to the proxy's or the host's (sandbox_proxy.html~L212, "the app origin MUST differ from BOTH trusted origins"). But those are last-ditch, and the user-visible result is a blank frame with a console throw. This needs a server-side check that refuses to advertise a sandbox URL whose origin matches any allowed UI origin, with the reason printed in the banner.2. Widening the referrer check is the delicate half — and it isn't optional.
ALLOWED_REFERRER_PATTERNinsandbox_proxy.htmlis hardcoded:/^http:\/\/(localhost|127\.0\.0\.1)(:|\/|$)/An
https://referrer cannot match that at all, so on any HTTPS deployment the proxy throws before anything else runs. Part 3 of the branch is therefore a prerequisite, not an optional extra. Doing it safely:new URL(document.referrer).originagainst an exact list. A regex over the raw referrer string is prefix/substring-attackable (https://evil.com/?x=https://inspector.example.com).<script>, soJSON.stringify+</script>escaping. This converts the proxy from static bytes into a template — acceptable, but the value also becomesEXPECTED_HOST_ORIGIN, which is the proxy's allowlist for parent messages.frame-ancestorsis already derived fromallowedOriginsviasandboxFrameAncestors(), and CSP — not the referrer — is the real control. So this aligns two lists that should already agree.ALLOWED_ORIGINSreplaces the defaults rather than merging, so a deployment that sets it for a public host silently loses loopback.Referrer-Policy: no-referreron the Inspector page breaks Apps outright, today and after.3. Trust model of the value itself.
The outer iframe deliberately has no
sandboxattribute (AppRenderer.tsx~L550), and the ext-apps host bridge posts withtargetOrigin: "*"(confirmed indist/src/app-bridge.js). So whatever origin this env var names becomes an unsandboxedpostMessagepeer of the Inspector page. That's tolerable for operator-supplied config — an operator already has strictly greater powers — but it argues for validating the shape (absolutehttp(s), no wildcard) rather than passing the string through raw. I would not give it aDANGEROUSLY_prefix: it doesn't widen a bind or disable auth, and the bad configurations should be rejected, not renamed.4. Scope gap the issue doesn't mention: the app-origin listener (
:6278) has the identical problem.Add
MCP_SANDBOX_FULL_ADDRESSalone and an app declaring_meta.ui.domainstill gets a URL built from the container's bind host. Per the README that is the one MCP Apps failure with no fallback — a blank frame, no console warning, because the cross-origin navigation failure isn't observable from the page. AlsoappDocumentEmbedders()feedsnew URL(sandboxUrl).origininto the published document'sframe-ancestors, so the override has to flow there or published documents get CSP-blocked. Either do both listeners, or explicitly document_meta.ui.domainas unsupported behind a proxy.5. Smaller things.
clients/web/static) is already fixed — it's in the root packlist as of Apps sandbox proxy omitted from package #1859. That part is stale.http://override under an HTTPS UI is silently blocked by the browser. Validate the scheme against the UI's and warn.…/sandboxmust be included, since the value replaces the whole URL) is a footgun — validate it or append it.MCP_PROXY_FULL_ADDRESS) is weaker than it looks: v1's proxy origin was not an isolation boundary the way the sandbox origin is, so "v1 had one" isn't sufficient justification on its own.Recommendation
Nothing here is a reason to refuse the request outright, but three things argue for leaving it in
Incomingrather than scheduling it::6278listener — all inside the sandbox, which is the surface where a mistake is a real isolation failure rather than a rendering bug, and which only two smokes (smoke:web:app,pack:verify) exercise at all.crypto.randomUUIDfinding above shows the Apps sandbox isn't even the first thing that breaks off-localhost. If we want to support that scenario, it should be scoped as such rather than approached one env var at a time.If we do take it, the honest framing is "make the Inspector deployable behind a reverse proxy", with this as one sub-task — and part 3 (the referrer check) probably deserves its own issue, since it's a standalone defect: the sandbox proxy cannot load from an HTTPS embedder at all.
fix(web): address Copilot review round 1 on public addresses (#1862)
docs: add the public-address vars to the environment-variable referen…
fix(web): only use a public address on a fixed listener port (#1862)
fix(web): point a refused sandbox embedder at ALLOWED_ORIGINS (#1862)
test(web): add a purpose header to the public-address suite (#1862)
cliffhall commentedon Sep 14, 2026
Shipped in #2370 (merged to
v2/main):MCP_SANDBOX_FULL_ADDRESSandMCP_APP_ORIGIN_FULL_ADDRESSadvertise public addresses for the MCP Apps listeners behind a reverse proxy, validated and refused when they would share the Inspector's origin, and the sandbox proxy's referrer check now accepts exactly theALLOWED_ORIGINSitsframe-ancestorsadmits, so an HTTPS Inspector page can load it. Documented indocs/environment-variables.mdand the web client README.feat(web): configurable public addresses for the MCP Apps listeners (m…