# Open-ended UI and MCP Apps

Course: [AI UX & Generative UI](https://www.devtutorials.ai/learn/ai-ux) · Section: Generative UI
Source: https://www.devtutorials.ai/learn/ai-ux/generative-ui/mcp-apps-sandbox

> A partner's MCP App arrives as full HTML. Host it in a sandboxed iframe with a CSP and message checks, so a malicious app cannot escape and a real one still works.

## What Open-ended UI Is

**Open-ended UI** = someone else (another company's server, or a tool) sends a complete piece of interface, as HTML with scripts, and your app shows it.

In this lesson, you will host a courier's app inside the order-support app, and lock it down so a fake one cannot steal data, trick the user or approve a refund.

**MCP App** = an MCP extension where a tool returns a small web app as a resource, and the host shows it in a sandboxed iframe.

A courier offers a slot picker as an MCP App. It is handy: the customer picks a delivery time right inside the chat. But you do not control its HTML. A look-alike app can try to escape.

Press **Play**, or use **Next** to go one step at a time.

- **Frame 1:** no sandbox. Every attempt works: the frame is part of your page.
- **Frame 2:** `allow-scripts allow-same-origin`. Still almost no protection.
- **Frames 3 to 5:** `allow-scripts` alone, then a CSP, then message checks. Each layer blocks more.

**Simple lesson:** open-ended UI is someone else's code in your app. Contain it.

### A Real-World Example

Think about a visitor in an office building.

- They get a badge that opens one meeting room, not every door.
- They can't take files out of the building.
- If they hand you a note "from the boss", you check it really is from the boss.

Hosting an MCP App works the same way:

- **One meeting room** = the iframe sandbox.
- **No files out of the building** = the Content Security Policy.
- **Checking the note** = validating messages from the frame.

Click the button. The Build tab loads the look-alike app with no sandbox.

- **The Escape attempts box:** each attempt, and whether it worked.
- **Side effects:** what the fake app got away with.
- **Messages from the app:** a fake "approve-refund" message.

**Simple lesson:** a frame without a sandbox is not a boundary.

## What You Will Learn

In this lesson, you will learn:

1. Why open-ended UI is the riskiest pattern.
2. Which iframe sandbox flags contain untrusted HTML.
3. Why two flags together undo the sandbox.
4. How a CSP stops data leaving.
5. How to check messages from the frame.

**About the attacks in this lab**

- The six escape attempts are simulated with written-out rules that match how browsers treat iframe sandbox flags.
- Real browsers enforce these rules themselves. Your job is to choose the right flags, and test your host in Chromium, Firefox and WebKit.
- The AI Security course goes deeper into attacks like these. The MCP course covers how MCP Apps are served.

## The Iframe Sandbox

**Sandbox** = an iframe attribute that turns off powers by default. Each flag you add turns one power back on.

| sandbox value | Scripts run? | Can reach your page? | Use it? |
|---|---|---|---|
| (none) | Yes | Yes | Never, for untrusted HTML |
| `allow-scripts allow-same-origin` | Yes | Yes, and it can remove its own sandbox | Never, for untrusted HTML |
| `allow-scripts` | Yes | No: it gets its own origin | Yes |
| `""` (empty) | No | No | Only for HTML with no scripts |

### The Dangerous Pair

`allow-same-origin` lets the frame keep your page's origin. With `allow-scripts` too, its script can reach your page, read cookies, and even take the sandbox off itself.

So for someone else's HTML, use `allow-scripts` on its own.

### Too Tight Is Broken

An empty sandbox blocks scripts, so it blocks every attack. It also blocks the real app.

Step through both frames.

- **Frame 1:** with no scripts, the courier's real slot picker is blank.
- **Frame 2:** with `allow-scripts`, a CSP and message checks, the fake app is contained and the real one works.

**Simple lesson:** `allow-scripts` alone: the app runs, your page is out of reach.

## The Content Security Policy

**CSP** = rules that say where a page may load from and send data to.

The sandbox keeps the frame away from your page. But the frame can still call any website and send what it collected. A CSP in the frame's HTML blocks that:

```tsx
const csp = `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:">`;

<iframe title="Courier slot picker" sandbox="allow-scripts" srcDoc={csp + appHtml} />
```

`default-src 'none'` means: no network calls unless you allow them. Add only the origins the app really needs.

**Check how your host serves the app.**

- Some hosts serve MCP Apps from a separate domain with their own CSP header.
- Others use `srcdoc`, like the example above.
- Read your host's docs, and test that a call to an unknown site really fails.

**Simple lesson:** the sandbox keeps it in; the CSP keeps your data in.

## Checking Messages

The frame talks to your app with `postMessage`. A fake app can post `{ type: "approve-refund" }` and hope your page believes it.

Step through both frames.

- **Frame 1:** the fake app's message comes from the wrong origin, with an unknown type. Rejected.
- **Frame 2:** the real app's `slot-picked` message comes from the courier's origin. Accepted.

```tsx
window.addEventListener("message", (e) => {
  if (e.source !== frame.current?.contentWindow) return;   // only our frame
  if (!ALLOWED_TYPES.has(e.data?.type)) return;            // only known messages
  onMessage(e.data);
});
```

Never let a message from the frame trigger a risky action on its own. Show a preview and ask, as in Lesson 5.

**Easy way to remember:** **Sandbox it, fence it, check its notes.** allow-scripts, a CSP, and message checks.

**Simple lesson:** believe only known messages, from your own frame.

### Lab Exercise: Lock Down the MCP App Host

Your task: change the Lesson 9 settings in the Build tab so both scenarios pass. Then press **Check all scenarios**.

Click the button and open the **Build** tab.

- **iframe sandbox:** pick the value that runs the app but keeps your page out of reach.
- **CSP and message checks:** turn both on.
- **Both scenarios:** the fake app must be contained, and the real one must still work.

**Simple lesson:** contain the app without breaking it.

### What Changes per Framework

| Option | Open-ended UI |
|---|---|
| Hand-built React | An iframe with `sandbox="allow-scripts"`, a CSP and a message check, as above |
| MCP Apps SDK | Host helpers for MCP App resources; still check its sandbox settings |
| CopilotKit | Shows MCP Apps carried over AG-UI |
| Claude and ChatGPT | Both hosts support MCP Apps for their own clients |

## Build It in React

In the **Code** tab, an `mcp-app` event brings the app's page as HTML. Your host decides what that page may do. Three lines do most of the work:

```tsx
<iframe ref={frame} sandbox="allow-scripts" srcDoc={CSP + app.html} />
```

```tsx
if (e.source !== frame.current?.contentWindow) return;   // only this frame
if (e.data?.type === "slot-picked") setSlot(e.data.slot);  // only known messages
```

The look-alike app in the second scenario really tries to read your page, send data out and approve a refund. The checks report which tries worked.

## TL;DR

1. **Open-ended UI** = someone else's HTML and scripts in your app. Highest risk.
2. **MCP App** = a small web app from an MCP tool, shown in a sandboxed iframe.
3. **Sandbox** = `allow-scripts` on its own. Never add `allow-same-origin` for untrusted HTML.
4. **CSP** = stops the frame sending data out.
5. **Message check** = only known types, only from your frame.
6. **Too tight** = an empty sandbox breaks the real app.

## Common mistakes

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| No sandbox | "It's from a partner, so it's fine." | Sandbox every third-party frame. | A compromised partner reads your page. |
| allow-scripts with allow-same-origin | "It needs both to work." | Use allow-scripts alone. | The frame removes its own sandbox. |
| No CSP | "The sandbox covers it." | Add a CSP so data can't leave. | Collected data is sent to another site. |
| Trusting every message | "Only our frame posts to us." | Check the source and the type. | A fake "approve-refund" is believed. |
| An empty sandbox | "Safest is best." | Allow scripts, contain the rest. | The real app is blank. |

## Check Your Understanding

## Exercises

## What to Remember

- Open-ended UI is someone else's code in your app.
- Use `sandbox="allow-scripts"` on its own.
- Never add `allow-same-origin` to untrusted HTML that runs scripts.
- Add a CSP so data cannot leave.
- Accept only known messages from your own frame.
- Test your host in every browser you support.

## Further Reading

- [MCP Apps, by the Model Context Protocol team](https://blog.modelcontextprotocol.io/posts/2025-11-21-mcp-apps/): what MCP Apps are and how hosts show them.
- [The iframe element and its sandbox attribute, on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe): every sandbox flag explained.

Next: **The AG-UI Channel**, where you wire the agent and the UI over one event stream, with state updates that never erase what the user typed.
