Skip to content
Merged
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
44 changes: 36 additions & 8 deletions samples/mcp/a2ui-in-mcpapps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ This sample demonstrates a Model Context Protocol (MCP) Application Host that is

- **`client/`**: The host container application (Angular). It hosts the outer safe iframe.
- **`server/`**: The MCP Server (Python/uv) that provides the micro-app resources and tools.
- **`server/apps/src/`**: The source source code for the server-hosted isolated micro-app.
- **`server/apps/src/`**: Source code for the **Basic** isolated micro-app.
- **`server/apps/editor/`**: Source code for the **Editor** isolated micro-app.

## Communication Flow

Expand Down Expand Up @@ -50,14 +51,31 @@ sequenceDiagram

## Prerequisites

- [Node.js](https://nodejs.org/) (runs the client and build scripts)
- [Python 3.10+](https://www.python.org/) with `uv` (runs the MCP server)
- [Node.js](https://nodejs.org/) (LTS recommended)
- [Python 3.10+](https://www.python.org/) with `uv`

### ⚠️ IMPORTANT: Pre-build Core Dependencies

The sample apps link to local versions of the A2UI SDK. You **must build the core libraries** before attempting to run `npm install` inside any sample subdirectories.

Run the following from the repository root:

```bash
# 1. Web Core
cd renderers/web_core && npm install && npm run build && cd ../..

# 2. Markdown Utilities
cd renderers/markdown/markdown-it && npm install && npm run build && cd ../../..

# 3. Angular Renderer SDK
cd renderers/angular && npm install && npm run build && cd ../..
```

---

## Build & Regeneration

This sample relies on some generated bundle artifacts. Some are committed for convenience, while others are ignored and must be built.
This sample relies on generated bundle artifacts.

### 1. Build Client Sandbox Bridge

Expand All @@ -71,19 +89,29 @@ npm run build:sandbox

_(Generates `client/public/sandbox_iframe/sandbox.{js,html}`)_

### 2. Rebuild the Server Hosted App (Optional)
### 2. Rebuild Micro-Apps (Optional)

The server serves single-file HTML artifacts located in `server/apps/public/`. Choose the app you want to build:

#### Option A: The Editor App

```bash
cd server/apps/editor
npm install
npm run build:all
```

The server serves a bundled `app.html` artifact located in `server/apps/public/app.html`. If you modify the source code in `server/apps/src/`, you must regenerate this list:
_(Generates `server/apps/public/editor.html`)_

Run this in the `server/apps/src/` directory:
#### Option B: The Basic App

```bash
cd server/apps/src
npm install
npm run build:all
```

_(Runs Angular compilation and triggers `node inline.js` to single-file inline it into `server/apps/public/app.html`)_
_(Generates `server/apps/public/app.html`)_

---

Expand Down
3 changes: 2 additions & 1 deletion samples/mcp/a2ui-in-mcpapps/client/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "npm"
"packageManager": "npm",
"analytics": "322f21ca-3fe1-45b7-b271-10523235cd8e"
},
"newProjectRoot": "projects",
"projects": {
Expand Down
17 changes: 16 additions & 1 deletion samples/mcp/a2ui-in-mcpapps/client/src/app/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,23 @@
<div class="header">
<h1>Simple MCP Apps Host</h1>
<p>Status: {{ status() }}</p>
<div style="margin-bottom: 15px; display: flex; gap: 10px; align-items: center">
<label for="app-selector" style="font-weight: bold">Select App:</label>
<select
id="app-selector"
#appSelector
(change)="onAppChange(appSelector.value)"
[disabled]="isAppLoading()"
style="padding: 8px; border-radius: 4px; border: 1px solid #ccc; font-size: 14px"
>
<option value="editor" [selected]="selectedApp() === 'editor'">
Generative Editor App
</option>
<option value="basic" [selected]="selectedApp() === 'basic'">Basic Counter App</option>
</select>
</div>
<button (click)="connectAndLoadApp()" [disabled]="isAppLoading()">
{{ isAppLoading() ? 'Loading...' : 'Load MCP App' }}
{{ isAppLoading() ? 'Loading...' : 'Load Selected App' }}
</button>
</div>

Expand Down
112 changes: 60 additions & 52 deletions samples/mcp/a2ui-in-mcpapps/client/src/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,17 @@ export class App implements AfterViewInit {
private messageListenerAdded = false;
protected readonly mcpAppHtmlUrl = signal<string | null>(null);
protected readonly isAppLoading = signal<boolean>(false);
protected readonly selectedApp = signal<'editor' | 'basic'>('editor');

private mcpClient: Client | null = null;

private readonly allowedTools = new Set([
'fetch_counter_a2ui',
'increase_counter',
'smart_editor_get_controls',
'smart_editor_apply',
]);

ngAfterViewInit() {
if (this.messageListenerAdded) return;
this.messageListenerAdded = true;
Expand Down Expand Up @@ -75,40 +83,50 @@ export class App implements AfterViewInit {
window.location.origin,
);
}
} else if (data?.method === 'ui/fetch_counter_a2ui') {
if (data.id && target && this.mcpClient) {
this.mcpClient
.callTool({
name: 'fetch_counter_a2ui',
arguments: {},
})
.then(result => {
target.postMessage(
{
jsonrpc: '2.0',
id: data.id,
result: result.content,
},
window.location.origin,
);
})
.catch(error => {
target.postMessage(
{
jsonrpc: '2.0',
id: data.id,
error: {message: error.message},
} else if (data?.method === 'ui/initialize') {
if (data.id && target) {
target.postMessage(
{
jsonrpc: '2.0',
id: data.id,
result: {
hostCapabilities: {
displayModes: ['inline'],
},
window.location.origin,
);
});
},
},
window.location.origin,
);
}
} else if (data?.method === 'ui/resize') {
const height = data.params?.height;
if (typeof height === 'number') {
iframe.style.height = `${height}px`;
}
} else if (data?.method?.startsWith('ui/')) {
// Generic tool relay for unknown verbs
const toolName = data.method.replace('ui/', '');

if (!this.allowedTools.has(toolName)) {
console.warn(`[Host] Blocked unauthorized tool call: ${toolName}`);
if (data.id && target) {
target.postMessage(
{
jsonrpc: '2.0',
id: data.id,
error: {message: `Tool '${toolName}' is not whitelisted.`},
},
window.location.origin,
);
}
return;
}
} else if (data?.method === 'ui/increase_counter') {

if (data.id && target && this.mcpClient) {
this.mcpClient
.callTool({
name: 'increase_counter',
arguments: {},
name: toolName,
arguments: data.params || {},
})
.then(result => {
target.postMessage(
Expand All @@ -131,30 +149,18 @@ export class App implements AfterViewInit {
);
});
}
} else if (data?.method === 'ui/initialize') {
if (data.id && target) {
target.postMessage(
{
jsonrpc: '2.0',
id: data.id,
result: {
hostCapabilities: {
displayModes: ['inline'],
},
},
},
window.location.origin,
);
}
} else if (data?.method === 'ui/resize') {
const height = data.params?.height;
if (typeof height === 'number') {
iframe.style.height = `${height}px`;
}
}
});
}

onAppChange(value: string) {
if (value === 'editor' || value === 'basic') {
this.selectedApp.set(value);
} else {
console.error(`[Host] Invalid app selected: ${value}`);
}
}
Comment thread
sugoi-yuzuru marked this conversation as resolved.

async connectAndLoadApp() {
this.status.set('Connecting to MCP Server...');
this.isAppLoading.set(true);
Expand All @@ -164,7 +170,7 @@ export class App implements AfterViewInit {
const transport = new SSEClientTransport(new URL('http://127.0.0.1:8000/sse'));
const client = new Client(
{
name: 'basic-host',
name: 'editor-host',
version: '1.0.0',
},
{
Expand All @@ -176,10 +182,12 @@ export class App implements AfterViewInit {
await client.connect(transport);
this.mcpClient = client;

this.status.set('Calling get_basic_app tool...');
this.status.set('Calling MCP App tool...');
const toolName = this.selectedApp() === 'editor' ? 'get_editor_app' : 'get_basic_app';

// 2. Call the tool to get the app
const result = await client.callTool({
name: 'get_basic_app',
name: toolName,
arguments: {},
});

Expand Down
104 changes: 104 additions & 0 deletions samples/mcp/a2ui-in-mcpapps/server/apps/editor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# A2UI-Powered Document Editor Micro-App

This directory contains the source code for the generative document editor micro-app based on Angular and Editor.js.

It is built as a standalone static bundle and served by the MCP server as an isolated resource to be rendered securely within the host container.

---

## Prerequisites

### 1. Node.js

Ensure you have Node.js installed (LTS v20 or v22 is recommended).

If Node.js is missing from your PATH, consider installing it via NVM (Node Version Manager):

```bash
# Download and install NVM
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash

# Refresh terminal
source ~/.bashrc

# Install LTS Node.js
nvm install --lts
```

### 2. Build Core A2UI Libraries

This application has file-based dependencies on the core A2UI packages which reside elsewhere in this repository. You **must build these packages first** in specific order before this application's `npm install` will succeed.

Run the following commands from the **root of the repository**:

```bash
# 1. Build Web Core
cd renderers/web_core
npm install
npm run build

# 2. Build Markdown Utilities
cd renderers/markdown/markdown-it
npm install
npm run build

# 3. Build Angular Renderer
cd renderers/angular
npm install
npm run build

cd ../../
```

---

## Local Execution Workflow

To execute and run this application locally, you need to build the application bundle, ensure the host environment assets are ready, and then boot the services.

### Step 1: Build the Editor App Bundle

From inside this directory (`samples/mcp/a2ui-in-mcpapps/server/apps/editor`):

```bash
# Install local package dependencies
npm install --legacy-peer-deps

# Build Angular project AND generate the single self-contained HTML file
npm run build:all
```

_This outputs the final static artifact into `../public/editor.html` which the Python server reads._

### Step 2: Build the Client Host Bridge (Required Once)

Navigate to the client host directory and build its security-sandbox bridge:

```bash
cd ../../../client
npm install
npm run build:sandbox
```

### Step 3: Run the Full Local Environment

Open two terminals to start the stack:

#### Terminal A: Run MCP Server (Python)

```bash
cd samples/mcp/a2ui-in-mcpapps/server
uv sync
uv run python server.py --transport sse --port 8000
```

#### Terminal B: Run Host Web App (Angular)

```bash
cd samples/mcp/a2ui-in-mcpapps/client
npm run start
```

#### Access the Application

Visit **[http://localhost:4200](http://localhost:4200)** in your browser to load the host container, which will automatically load this Editor app via the MCP server connection.
Loading
Loading