fix(client): sandbox proxy speaks the current bridge protocol - #214
fix(client): sandbox proxy speaks the current bridge protocol#214witt3rd wants to merge 3 commits into
Conversation
The reference sandbox proxy (scripts/proxy/index.html) used a pre-1.7
postMessage protocol that no longer matches the @modelcontextprotocol/ext-apps
bridge @mcp-ui/client depends on, so any host copying it got a ready-handshake
timeout and a blank card.
The proxy posted { type: 'ui-proxy-iframe-ready' } and listened for
{ type: 'ui-html-content' }, while the bridge (setupSandboxProxyIframe /
AppBridge.sendSandboxResourceReady) waits for a message whose .method equals
SANDBOX_PROXY_READY_METHOD ('ui/notifications/sandbox-proxy-ready') and delivers
HTML as the JSON-RPC notification
{ method: 'ui/notifications/sandbox-resource-ready', params: { html, csp } }.
Different field (type vs method) and different strings → onReady never resolves,
AppFrame times out, no HTML is ever sent.
Fix (rawhtml mode only; external-url branch untouched):
- emit { method: 'ui/notifications/sandbox-proxy-ready' } as the ready signal
- read HTML from event.data.params.html on the
'ui/notifications/sandbox-resource-ready' notification
ProxyScript.test.ts now imports the real SANDBOX_PROXY_READY_METHOD constant
from @modelcontextprotocol/ext-apps/app-bridge and sends HTML in the bridge's
notification shape, so it fails if the proxy ever drifts from the SDK's wire
format again. Full client suite: 74/74 passing.
…ame PR)
The protocol-string fix was necessary but not sufficient: the proxy still gated
raw-HTML mode behind ?contentType=rawhtml, but the host component (AppFrame /
buildSandboxUrl) sets NEITHER contentType NOR url — it delivers HTML via
postMessage (sendSandboxResourceReady). So the gate never fired: the proxy fell
through to 'Error: missing url or html parameter', never posted the ready
signal, and AppFrame timed out with a blank card ('timed out waiting for sandbox
proxy iframe to render'). Confirmed: no production caller in the SDK sets either
query param (three empty greps across components/ + utils/).
Fix: raw-HTML is now the DEFAULT mode, taken whenever there is no explicit ?url=
target. The external-URL path is the opt-in (an actual target present).
?contentType=rawhtml is still honored for callers that set it, but is no longer
required — matching how AppFrame actually drives the proxy.
ProxyScript.test.ts gains a regression guard: loads the proxy with NO query
params (exactly how AppFrame calls it) and asserts it enters raw-HTML mode
(posts ready + creates iframe#root). Proven both directions: the test FAILS
against the pre-fix proxy and PASSES with the fix. Full client suite: 75/75.
Verified end-to-end in a live consumer (spire-ui): the fixed proxy served
statically, AppRenderer renders real specialist HTML in the sandboxed iframe —
the card paints (human-confirmed in-browser), where before it was blank.
|
Second drift found + fixed in this PR (611960f). The protocol-string fix was necessary but not sufficient — a live consumer (spire-ui rendering via Root: the proxy gated raw-HTML mode behind Fix: raw-HTML is now the default mode (taken when there's no explicit Regression guard: Verified in a live browser: fixed proxy served by spire-ui, |
Review of a downstream consumer (spire-ui PR) flagged a pre-existing issue in the reference proxy: messages relayed from the guest iframe back to the parent used targetOrigin='*', so a malicious site that iframes /sandbox_proxy.html could receive relayed guest messages under the embedder's context. Derive hostOrigin once from document.referrer (the embedding parent's URL), fall back to '*' only when referrer is stripped, and use it as the targetOrigin for both parent-directed relays (raw-HTML mode + external-URL mode). The stateless ready handshake stays '*': it carries no data, and the host origin isn't reliably knowable at signal time. Note: the external-URL relay is scoped to hostOrigin (the parent is the host app), NOT the guest's urlOrigin — posting guest→parent with the guest's origin would be incorrect. Full client suite: 75/75.
Fixes #213.
Problem
The reference sandbox proxy (
sdks/typescript/client/scripts/proxy/index.html) uses a pre-1.7 postMessage protocol that no longer matches the@modelcontextprotocol/ext-appsbridge that@mcp-ui/clientdepends on. A host that copies this proxy (the documented integration path) gets a ready-handshake timeout → blank card, because the bridge never sees the ready signal it waits for.What the proxy posted:
What the bridge waits for (
setupSandboxProxyIframe/AppBridge.sendSandboxResourceReady, verified againstext-apps@1.7.4):Different field (
typevsmethod) and different strings, soonReadynever resolves,AppFrametimes out atDEFAULT_SANDBOX_TIMEOUT_MS, and no HTML is ever sent.Fix
scripts/proxy/index.html, rawhtml mode only (the external-url branch is untouched):{ method: 'ui/notifications/sandbox-proxy-ready' }as the ready signal.event.data.params.htmlon theui/notifications/sandbox-resource-readynotification.The double-iframe structure,
document.writerendering, and bidirectional relay are unchanged — only the three stale protocol strings are corrected.Test hardening
ProxyScript.test.tspreviously asserted the proxy's own stale strings, which is why it stayed green while the proxy drifted from the bridge. It now:SANDBOX_PROXY_READY_METHODfrom@modelcontextprotocol/ext-apps/app-bridge(so it can't drift from the SDK again), and{ method, params: { html } }).Verification: full
@mcp-ui/clientsuite — 74/74 passing (vitest run).Note
Filed from a fork; happy to adjust the shape to your preferences. If you'd also like the proxy shipped as a package export (so integrators reference a version-matched file rather than copying a
scripts/artifact), I can add that in a follow-up.