Skip to content

MCP App sandbox omits allow-same-origin, so apps and their embeds run with a null origin #4271

Open

Description

@jroxbury

Short summary

MCP App views render in an <iframe> with sandbox="allow-scripts allow-popups allow-forms". Because allow-same-origin is missing, every MCP App, and every frame it embeds, runs with an opaque null origin. Browser storage throws, and cross-origin requests send Origin: null, which origin-checking services reject. The MCP Apps spec requires the sandbox to have allow-scripts and allow-same-origin.

Affected version or release

1.1.23 (Windows)

Installation context

Windows 11 Enterprise 25H2 (build 26200.9448), WebView2 Runtime 154.0.4258.37, desktop installation. Local session with a custom MCP server that exposes an MCP App resource (text/html;profile=mcp-app), shown as an inline MCP App view.

What happened?

  • The MCP App <iframe> loads from http://mcp-app-guest.localhost/<token> with sandbox="allow-scripts allow-popups allow-forms".
  • Inside the app, window.origin is "null" and localStorage throws:
    SecurityError: Failed to read the 'localStorage' property from 'Window': The document is sandboxed and lacks the 'allow-same-origin' flag.
  • Sandbox flags are inherited, so a cross-origin page the app embeds (allowed via _meta.ui.csp.frameDomains) is also opaque, even if its own <iframe> includes allow-same-origin. Its API calls send Origin: null. In our case, the embedded service rejects the CORS preflight with HTTP 400 and the embed never renders.
  • App authors can't work around this. The flags are set on the host's <iframe>, a frame can't relax its parent's sandbox, and MCP Apps resource metadata has no field for sandbox flags.

As a local check, adding allow-same-origin to the MCP App <iframe> at runtime through DevTools (only for mcp-app-guest.localhost frames, no other change) makes the same app and its embed load and render normally. The same app also works in a host that uses the MCP Apps reference host's sandbox (allow-scripts allow-same-origin allow-forms).

Steps to reproduce

  1. Start the Copilot app with WebView2 DevTools enabled. For example, set WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=9223 before launching, then open edge://inspect.
  2. Call any tool that renders an MCP App.
  3. Inspect the MCP App <iframe>. Its sandbox is allow-scripts allow-popups allow-forms.
  4. In the console, switch to the MCP App frame and run window.origin (returns "null") and localStorage.length (throws SecurityError).

Or add this probe to any MCP App's HTML:

<pre id="probe"></pre>
<script>
  const lines = [`window.origin: ${window.origin}`];
  try { localStorage.setItem('probe', '1'); lines.push('localStorage: ok'); }
  catch (e) { lines.push(`localStorage: ${e.name}`); }
  document.getElementById('probe').textContent = lines.join('\n');
</script>

In Copilot it prints window.origin: null and localStorage: SecurityError.

Expected behavior

MCP App views get a real, non-opaque origin, as the MCP Apps spec requires (2026-01-26, Sandbox proxy):

  1. The Host and the Sandbox MUST have different origins.
  2. The Sandbox MUST have the following permissions: allow-scripts, allow-same-origin.

The reference host uses allow-scripts allow-same-origin allow-forms (basic-host sandbox.ts), and the proposed conformance tests assert a non-opaque app origin (modelcontextprotocol/ext-apps#743).

Copilot already meets the first rule: apps load from mcp-app-guest.localhost, not the app shell's origin. It also honors _meta.ui.csp.frameDomains by adding those origins to the app's frame-src, so embedding cross-origin pages is supported. But those pages inherit the opaque origin, so embeds that call their own APIs or use storage fail.

Additional context

Likely cause: This looks like a deliberate isolation choice rather than an oversight. Every MCP App loads from the same origin (mcp-app-guest.localhost), so adding allow-same-origin alone would let apps from different servers share storage and script each other's frames. Leaving it out prevents that, but breaks spec conformance and any app that needs a real origin.

Suggested fix: Give each MCP server (or each app) its own origin, for example <server-id>.mcp-app-guest.localhost, then add allow-same-origin. See the discussion on unique sandbox origins in modelcontextprotocol/ext-apps#213.

Who is affected: Self-contained apps that only render tool data work today. Apps that use browser storage, call origin-checked or cookie-based APIs, or embed authenticated third-party content fail. The MCP Apps docs recommend localStorage for view-state persistence (patterns), which fails in Copilot.

Possibly related: #781 (MCP app canvas does not render), unverified.

Logs: Debug logs aren't attached because they include details of a private MCP server. The behavior reproduces with any MCP App using the steps above. I can share sanitized logs if needed.

Activity

  1. added theissue type on Sep 28, 2026
  2. roneetshaw commented on Oct 8, 2026

    @roneetshaw

    I was also able to reproduce this with our Power BI Report Authoring MCP App on
    Windows in the GitHub Copilot desktop app.

    Our component exposes open_report_view, which returns an MCP App resource
    (text/html;profile=mcp-app). The app uses powerbi-client to render a
    published Power BI report inside a nested iframe. Its MCP resource metadata
    declares the required Power BI domains through _meta.ui.csp (frameDomains,
    connectDomains, and resourceDomains), and sensitive embed credentials are
    delivered through private MCP result metadata/resources rather than included in
    the shared HTML or model-facing response.

    Before isolating the sandbox behavior, we verified all of the following
    independently:

    • The MCP resolved the expected workspace, report, and page.
    • The generated Power BI/MSIT embed URL was correct.
    • The caller-bound Power BI access token was valid.
    • The MCP App HTML and CSP metadata were loaded by Copilot.
    • The same report, embed URL, token, and powerbi-client flow rendered
      successfully in standalone Microsoft Edge.
    • Expanding the declared MCP App CSP domains did not fix rendering in Copilot.

    Inside the generic Copilot MCP App view, Power BI failed with:

    NetworkConnectionIssue
    Get report failed
    

    We then reduced the scenario to a controlled iframe A/B test using the same
    viewer and report:

    <iframe sandbox="allow-scripts">

    Result: the Power BI embed failed with the same
    NetworkConnectionIssue: Get report failed behavior.

    <iframe sandbox="allow-scripts allow-same-origin">

    Result: the report rendered successfully.

    This confirms the same root cause described in this issue: without
    allow-same-origin, the outer sandbox gives the MCP App an opaque origin, and
    that restriction is inherited by its embedded Power BI frame. Adding
    allow-same-origin only to the nested iframe cannot relax the parent sandbox.

    We also confirmed that the MCP transport was not the cause. The MCP was
    reachable through a Dev Tunnel, tool discovery and resource delivery worked,
    and the report credentials were valid. A tunnel can transport MCP requests, but
    it cannot change the sandbox flags applied by the Copilot host. Likewise, the
    MCP App's CSP metadata can authorize domains but cannot request a different
    iframe sandbox policy.

    As a product-specific workaround, we built/used a separately installed GitHub
    Copilot client canvas extension. That extension reads the same MCP private
    report-credential resource through the connected MCP API and renders the report
    through a dedicated client canvas rather than the generic MCP App iframe. The
    report works through that canvas path. However, this is not a workaround
    available to a normal MCP App author and does not fix open_report_view; it
    requires a host-specific client extension.

    So our observations are consistent with the report here:

    1. The MCP server, token, report lookup, CSP, and embedded service can all be
      valid.
    2. The generic Copilot MCP App host still fails because the parent iframe omits
      allow-same-origin.
    3. App code, a nested iframe, CSP metadata, and the MCP transport cannot correct
      the parent sandbox.
    4. Adding allow-same-origin in the controlled host configuration is the single
      change that made the same embed render.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions