Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/assets/mcp-apps-calculator-demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
161 changes: 134 additions & 27 deletions docs/guides/mcp-apps-in-a2ui-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ This guide explains how **Model Context Protocol (MCP) Applications** are integr

The Model Context Protocol (MCP) allows MCP servers to deliver rich, interactive HTML-based user interfaces to hosts. A2UI provides a secure environment to run these third-party applications.

<img src="../assets/mcp-apps-calculator-demo.gif" alt="MCP Calculator demo — loading the app, opening the calculator, and chatting with the agent" width="640">

## Double-Iframe Isolation Pattern

To run untrusted third-party code securely, A2UI utilizes a **double-iframe** isolation pattern. This approach isolates raw DOM injection from the main application while maintaining a structured JSON-RPC channel.
Expand All @@ -21,7 +23,7 @@ To prevent this, A2UI strictly excludes `allow-same-origin` for the inner iframe
### The Architecture

1. **[Sandbox Proxy (`sandbox.html`)](https://github.com/google/A2UI/blob/main/samples/client/shared/mcp_apps_inner_iframe/sandbox.html)**: An intermediate `iframe` served from the same origin. It isolates raw DOM injection from the main app while maintaining a structured JSON-RPC channel.
- Permissions: **Do not sandbox** in the host template (e.g., [`mcp-app.ts`](https://github.com/google/A2UI/blob/main/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/mcp-app.ts) or [`mcp-apps-component.ts`](https://github.com/google/A2UI/blob/main/samples/client/lit/contact/ui/custom-components/mcp-apps-component.ts)).
- Permissions: **Do not sandbox** in the host template (e.g., [`mcp-app.ts`](https://github.com/google/A2UI/blob/main/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/mcp-app.ts) or [`mcp-apps-component.ts`](https://github.com/google/A2UI/blob/main/samples/client/lit/custom-components-example/ui/custom-components/mcp-apps-component.ts)).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The linked Lit example (mcp-apps-component.ts) contains two discrepancies with the security model described in this guide:

  1. Outer Iframe Sandboxing: The guide states that the host template should not sandbox the proxy iframe (line 24). However, the Lit component includes a sandbox attribute with allow-same-origin (line 79 of that file).
  2. Inner Iframe Isolation: The guide emphasizes that the inner iframe MUST NOT include allow-same-origin (lines 19 and 27). However, the Lit component explicitly includes it when calling sendSandboxResourceReady (line 179 of that file).

These should be corrected in the sample code to ensure it serves as a secure and consistent reference implementation. The Angular example (mcp-app.ts) correctly follows these principles.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — verified this is a real issue.

Angular sample correctly passes sandbox: 'allow-scripts' to sendSandboxResourceReady() (line ~89 of mcp-app.ts), so the inner iframe ends up with only allow-scripts.

Lit sample passes sandbox: 'allow-scripts allow-forms allow-popups allow-modals allow-same-origin' (line ~179 of mcp-apps-component.ts), which violates the guide's security model.

Additionally:

  • The outer iframe in Lit has sandbox="allow-scripts allow-forms allow-popups allow-modals allow-same-origin" (line 79) — the guide says the proxy iframe should not be sandboxed.
  • The shared sandbox.ts (line 66) sets an initial default of allow-scripts allow-same-origin allow-forms on the inner iframe before sendSandboxResourceReady overrides it — this means there's a brief window where the inner iframe has allow-same-origin.

This is a docs PR so fixing the Lit sample code is out of scope here, but it should be filed as a separate issue. The guide's security model description is correct — the Lit sample doesn't follow it.

- Host origin validation: Validates that messages come from the expected host origin.
2. **Embedded App (Inner Iframe)**: The innermost `iframe`. Injected dynamically via `srcdoc` with restricted permissions.
- Permissions: `sandbox="allow-scripts allow-forms allow-popups allow-modals"` (**MUST NOT** include `allow-same-origin`).
Expand Down Expand Up @@ -113,51 +115,156 @@ Because `allow-same-origin` is strictly omitted for the innermost iframe, the fo
## Prerequisites

To run the samples, ensure you have the following installed:
- **Python (uv)** (version 3.12 or higher suggested)
- **Node.js (npm)** (version 18 or higher recommended)

- **Python 3.10+** — Required for the agent and MCP server backends
- **[uv](https://docs.astral.sh/uv/)** — Fast Python package manager (used to run all Python samples)
- **Node.js 18+** and **npm** — Required for building and running the client apps
- **A `GEMINI_API_KEY`** — Required by all ADK-based agents. Get one from [Google AI Studio](https://aistudio.google.com/apikey)

> ⚠️ **Environment variable setup**: You can either export `GEMINI_API_KEY` in your shell or create a `.env` file in each agent directory. The agents use `dotenv` to load `.env` files automatically.
>
> ```bash
> # Option 1: Export in shell
> export GEMINI_API_KEY="your-api-key-here"
>
> # Option 2: Create .env file in the agent directory
> echo 'GEMINI_API_KEY=your-api-key-here' > .env
> ```

## Samples

There are two primary samples demonstrating MCP Apps integration:
There are two primary samples demonstrating MCP Apps integration. Each sample requires running **multiple terminals** — one for each backend service and one for the client.

---

### 1. Contact Multi-Surface Sample (Lit & ADK Agent)
### Sample 1: Contact Multi-Surface (Lit Client + ADK Agent)

This sample verifies the sandbox with a Lit-based client and an ADK-based A2A agent.

- **A2A Agent Server**:
- Path: [`samples/agent/adk/contact_multiple_surfaces/`](https://github.com/google/A2UI/tree/main/samples/agent/adk/contact_multiple_surfaces/)
- Command: `uv run .` (requires `GEMINI_API_KEY` in `.env`)
- **Lit Client App**:
- Path: [`samples/client/lit/contact/`](https://github.com/google/A2UI/tree/main/samples/client/lit/contact/)
- Command: `npm run dev` (requires building the Lit renderer first)
- URL: `http://localhost:5173/`
#### Step 1: Start the A2A Agent Server

```bash
cd samples/agent/adk/contact_lookup/
export GEMINI_API_KEY="your-key" # or use a .env file
uv run .
```

> ⚠️ **Python version**: This agent requires Python ≥ 3.13 (see its `pyproject.toml`). If `uv run .` fails with a Python version error, ensure you have Python 3.13+ available.

The agent starts on `http://localhost:10003` by default.

#### Step 2: Build the Lit Renderer

In a **new terminal**, build the renderers (required before the client can run):

```bash
cd samples/client/lit/
npm install
npm run build:renderer
```

> ⚠️ **First-time build**: The `build:renderer` script builds three packages (`web_core`, `markdown-it`, and `lit` renderer) in sequence. This may take a minute on the first run.

#### Step 3: Start the Lit Client

```bash
cd samples/client/lit/
npm run serve:shell
```

The client starts at `http://localhost:5173/`.

> 💡 **Shortcut**: You can run agent + client together with:
> ```bash
> cd samples/client/lit/
> npm run demo:contact
> ```
> This builds the renderer and starts both the shell and the contact_lookup agent concurrently.

**What to expect**: A contact page where actions prompt an app interface on specific interactions.

### 2. MCP Apps (Calculator) (Angular)
---

### Sample 2: MCP Calculator (Angular Client + MCP Server + Proxy Agent)

This sample verifies the sandbox with an Angular-based client, an MCP Proxy Agent, and a remote MCP Server. It requires **three** backend processes.

#### Step 1: Start the MCP Server (Calculator)

```bash
cd samples/agent/mcp/mcp-apps-calculator/
uv run .
```

This sample verifies the sandbox with an Angular-based client, an MCP Proxy Agent, and a remote MCP Server.
The MCP server starts on `http://localhost:8000` using SSE transport.

- **MCP Server (Calculator)**:
- Path: [`samples/agent/mcp/mcp-apps-calculator/`](https://github.com/google/A2UI/tree/main/samples/agent/mcp/mcp-apps-calculator/)
- Command: `uv run .` (runs on port 8000)
- **MCP Apps Proxy Agent**:
- Path: [`samples/agent/adk/mcp_app_proxy/`](https://github.com/google/A2UI/tree/main/samples/agent/adk/mcp_app_proxy/)
- Command: `uv run .` (requires `GEMINI_API_KEY` in `.env`)
- **Angular Client App**:
- Path: [`samples/client/angular/`](https://github.com/google/A2UI/tree/main/samples/client/angular/)
- Command: `npm start -- mcp_calculator` (requires `npm run build:sandbox` and `npm install`)
- URL: `http://localhost:4200/?disable_security_self_test=true`
#### Step 2: Start the MCP Apps Proxy Agent

In a **new terminal**:

```bash
cd samples/agent/adk/mcp_app_proxy/
export GEMINI_API_KEY="your-key" # or use a .env file
uv run .
```

The proxy agent starts on `http://localhost:10006` by default.

#### Step 3: Build and Start the Angular Client

In a **new terminal**:

```bash
cd samples/client/angular/

# Build the renderers (required — Angular depends on local renderer packages)
npm run build:renderer

npm install --include=dev
npm run build:sandbox
npm start -- mcp_calculator
```

> ⚠️ **`--include=dev` is required**: The Angular CLI (`@angular/cli`) is a dev dependency. Without `--include=dev`, `ng serve` won't be available.
>
> ⚠️ **`build:renderer` and `build:sandbox` are both required**: `build:renderer` compiles the A2UI renderer packages that the Angular app depends on. `build:sandbox` bundles the sandbox proxy into the Angular project's public assets. Without either, the app won't work.

The client starts at `http://localhost:4200/`.

#### Step 4: Open in Browser

Navigate to:

```
http://localhost:4200/?disable_security_self_test=true
```

**What to expect**: A basic calculator will be rendered. You can execute arithmetic calculations cleanly through the sandbox.

---

## URL Options for Testing

For testing purposes, you can opt-out of the security self-test by using specific URL query parameters.

`disable_security_self_test=true`
### `disable_security_self_test=true`

This query parameter allows you to bypass the security self-test that verifies iframe isolation. This is useful for debugging and testing environments.
This query parameter allows you to bypass the security self-test that verifies iframe isolation. This is useful for debugging and testing environments where the double-iframe setup may not pass strict origin checks (e.g., `localhost` development).

Example usage:
`http://localhost:4200/?disable_security_self_test=true`
```
http://localhost:4200/?disable_security_self_test=true
```

## Troubleshooting

| Problem | Solution |
|---------|----------|
| `GEMINI_API_KEY environment variable not set` | Export the key or add a `.env` file in the agent directory |
| Python version error on `contact_lookup` agent | Install Python 3.13+ (required by that sample's `pyproject.toml`) |
| `npm run build:renderer` fails | Make sure you ran `npm install` first in `samples/client/lit/` |
| Angular client shows blank page | Ensure you ran `npm run build:sandbox` before `npm start` |
| MCP app iframe doesn't load | Check that both the MCP server (port 8000) and proxy agent (port 10006) are running |
| `ng serve` not found | Run `npm install --include=dev` to install dev dependencies including `@angular/cli` |
| "URL with hostname not allowed" | Angular 21 restricts allowed hosts. Use `localhost` (the default) — do not pass `--host 0.0.0.0` |
| Security self-test fails in dev | Add `?disable_security_self_test=true` to the URL |
Loading