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
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ coverage/
a2a_agents/python/a2ui_agent/src/a2ui/assets/**/*.json
## new agent SDK path
agent_sdks/python/src/a2ui/assets/**/*.json
## Generated JS file from the strictly-typed `sandbox.ts`.
samples/client/angular/projects/orchestrator/public/sandbox_iframe/sandbox.js
## Generated JS file from the strictly-typed `sandbox.ts`.
samples/client/angular/projects/mcp_calculator/public/sandbox_iframe/sandbox.js
17 changes: 2 additions & 15 deletions samples/agent/adk/orchestrator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,30 +42,17 @@ Subagents are configured using RemoteA2aAgent which translates ADK events to A2A
uv run . --port=10005
```

Optionally, run the MCP Server and MCP App Proxy Agent to MCP Apps in A2UI demo:

```bash
cd samples/agent/mcp/calculator
uv run . --port=8000
```

```bash
cd samples/agent/adk/mcp_app_proxy
uv run . --port=10006
```

3. Run the orchestrator agent:

```bash
cd samples/agent/adk/orchestrator
uv run . --port=10002 --subagent_urls=http://localhost:10003 --subagent_urls=http://localhost:10004 --subagent_urls=http://localhost:10005 --subagent_urls=http://localhost:10006
uv run . --port=10002 --subagent_urls=http://localhost:10003 --subagent_urls=http://localhost:10004 --subagent_urls=http://localhost:10005
```

4. Try commands that work with any agent:
a. "Who is Alex Jordan?" (routed to contact lookup agent)
b. "Show me chinese food restaurants in NYC" (routed to restaurant finder agent)
c. "Show my sales data for Q4" (routed to rizzcharts)
d. "Open calculator" (routed to mcp app proxy)

## Disclaimer

Expand All @@ -75,4 +62,4 @@ All operational data received from an external agent—including its AgentCard,

Similarly, any UI definition or data stream received must be treated as untrusted. Malicious agents could attempt to spoof legitimate interfaces to deceive users (phishing), inject malicious scripts via property values (XSS), or generate excessive layout complexity to degrade client performance (DoS). If your application supports optional embedded content (such as iframes or web views), additional care must be taken to prevent exposure to malicious external sites.

Developer Responsibility: Failure to properly validate data and strictly sandbox rendered content can introduce severe vulnerabilities. Developers are responsible for implementing appropriate security measures—such as input sanitization, Content Security Policies (CSP), strict isolation for optional embedded content, and secure credential handling—to protect their systems and users.
Developer Responsibility: Failure to properly validate data and strictly sandbox rendered content can introduce severe vulnerabilities. Developers are responsible for implementing appropriate security measures—such as input sanitization, Content Security Policies (CSP), strict isolation for optional embedded content, and secure credential handling—to protect their systems and users.
1 change: 1 addition & 0 deletions samples/client/angular/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Here are the instructions if you want to do each step manually.
* `npm start -- contact`
* `npm start -- rizzcharts`
* `npm start -- orchestrator`
* `npm run build:sandbox && npm start -- mcp_calculator`
* `npm start -- gallery` (Client-only, no server required)
5. Open http://localhost:4200/

Expand Down
110 changes: 110 additions & 0 deletions samples/client/angular/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,116 @@
}
}
}
},
"mcp_calculator": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss",
"skipTests": true
},
"@schematics/angular:class": {
"skipTests": true
},
"@schematics/angular:directive": {
"skipTests": true
},
"@schematics/angular:guard": {
"skipTests": true
},
"@schematics/angular:interceptor": {
"skipTests": true
},
"@schematics/angular:pipe": {
"skipTests": true
},
"@schematics/angular:resolver": {
"skipTests": true
},
"@schematics/angular:service": {
"skipTests": true
}
},
"root": "projects/mcp_calculator",
"sourceRoot": "projects/mcp_calculator/src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"browser": "projects/mcp_calculator/src/main.ts",
"tsConfig": "projects/mcp_calculator/tsconfig.app.json",
"preserveSymlinks": true,
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "projects/mcp_calculator/public"
}
],
"styles": [
"projects/mcp_calculator/src/styles.scss"
],
"server": "projects/mcp_calculator/src/main.server.ts",
"outputMode": "server",
"ssr": {
"entry": "projects/mcp_calculator/src/server.ts"
}
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500KB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "mcp_calculator:build:production"
},
"development": {
"buildTarget": "mcp_calculator:build:development"
}
},
"defaultConfiguration": "development"
},
"test": {
"builder": "@angular/build:karma",
"options": {
"codeCoverage": true,
"tsConfig": "projects/mcp_calculator/tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "projects/mcp_calculator/public"
}
],
"styles": [
"projects/mcp_calculator/src/styles.scss"
]
}
}
}
}
},
"cli": {
Expand Down
8 changes: 2 additions & 6 deletions samples/client/angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,7 @@
"build:renderer": "cd ../../../renderers && for dir in 'web_core' 'markdown/markdown-it'; do (cd \"$dir\" && npm install && npm run build); done",
"serve:agent:restaurant": "cd ../../agent/adk/restaurant_finder && uv run .",
"demo:restaurant": "npm run build:renderer && concurrently -k -n \"AGENT,WEB\" -c \"magenta,blue\" \"npm run serve:agent:restaurant\" \"npm start -- restaurant\"",
"serve:agent:contact": "cd ../../agent/adk/contact_lookup && uv run .",
"demo:contact": "npm run build:renderer && concurrently -k -n \"AGENT,WEB\" -c \"magenta,blue\" \"npm run serve:agent:contact\" \"npm start -- contact\"",
"serve:agent:rizzcharts": "cd ../../agent/adk/rizzcharts && uv run .",
"demo:rizzcharts": "npm run build:renderer && concurrently -k -n \"AGENT,WEB\" -c \"magenta,blue\" \"npm run serve:agent:rizzcharts\" \"npm start -- rizzcharts\"",
"build:sandbox": "esbuild projects/orchestrator/public/sandbox_iframe/sandbox.ts --bundle --outfile=projects/orchestrator/public/sandbox_iframe/sandbox.js --format=esm --platform=browser"
"build:sandbox": "esbuild projects/mcp_calculator/public/sandbox_iframe/sandbox.ts --bundle --outfile=projects/mcp_calculator/public/sandbox_iframe/sandbox.js --format=esm --platform=browser"
},
"prettier": {
"printWidth": 100,
Expand Down Expand Up @@ -93,4 +89,4 @@
"../../../renderers/web_core",
"../../../renderers/markdown/markdown-it"
]
}
}
23 changes: 23 additions & 0 deletions samples/client/angular/projects/mcp_calculator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# MCP Calculator

Sample application using the Chat-Canvas component with MCP Calculator Agent.

## Prerequisites

1. [nodejs](https://nodejs.org/en)
2. An endpoint hosting the MCP Calculator A2AService. ([Review the instructions on how to run MCP Calculator A2AService](../../../../agent/adk/mcp_app_proxy/README.md).)

## Running

1. Build the shared dependencies by running `npm run build` in the `renderers/web_core` directory
2. Install the dependencies: `npm i`
3. Run the A2A server for all of the agents. ([Link to instructions](../../../../agent/adk/mcp_app_proxy/README.md))
4. Build the `sandbox.js` for testing MCP Apps in A2UI demo

- `npm run build:sandbox`

5. Run the app:

- `npm start -- mcp_calculator`

6. Open http://localhost:4200/
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This directory contains the `sandbox.html` and its associated resources.

`sandbox.html` is designed to be loaded into an `<iframe>` to provide a secure,
isolated environment for running MCP (Model Context Protocol) applications. It
acts as a bridge between the host application (Orchestrator) and the untrusted
acts as a bridge between the host application (MCP Calculator) and the untrusted
or external MCP apps, managing communication via `postMessage`.

## Development
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Catalog } from '@a2ui/angular';
import { inputBinding } from '@angular/core';

export const DEMO_CATALOG = {
McpApp: {
type: () => import('./mcp-app').then((r) => r.McpApp),
bindings: ({ properties }) => [
inputBinding(
'content',
() => ('content' in properties && properties['content']) || undefined,
),
inputBinding('title', () => ('title' in properties && properties['title']) || undefined),
],
},
} as Catalog;
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ export class McpApp

// Set src to trigger load AFTER listener is ready
// TODO: Make the sandbox URL configurable. To ensure CORS encapsulation, the sandbox
// should be served from a different origin than the orchestrator.
// should be served from a different origin than the host app.
const sandboxUrl = 'sandbox_iframe/sandbox.html';
this.iframeSrc.set(
this.sanitizer.bypassSecurityTrustResourceUrl(sandboxUrl),
Expand All @@ -164,7 +164,7 @@ export class McpApp
const emptyMcpClient = null;
const bridge = new AppBridge(
emptyMcpClient,
{ name: 'A2UI Orchestrator', version: '1.0.0' },
{ name: 'MCP Calculator', version: '1.0.0' },
{
openLinks: {},
logging: {},
Expand Down Expand Up @@ -206,8 +206,8 @@ export class McpApp
// Pseudo-code for dispatch:
// const actionName = params.name;
// if (this.allowedTools().includes(actionName)) {
// // Dispatch action to A2UI orchestrator/store
// // events.dispatch('a2ui.action', { name: actionName, ... });
// // Dispatch action to host store
// // events.dispatch('host.action', { name: actionName, ... });
// return { content: [{ type: "text", text: "Action dispatched" }] };
// } else {
// console.warn(`Tool '${actionName}' blocked.`);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { ApplicationConfig, mergeApplicationConfig } from '@angular/core';
import { provideServerRendering, withRoutes } from '@angular/ssr';
import { appConfig } from './app.config';
import { serverRoutes } from './app.routes.server';

const serverConfig: ApplicationConfig = {
providers: [provideServerRendering(withRoutes(serverRoutes))],
};

export const config = mergeApplicationConfig(appConfig, serverConfig);
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {
configureChatCanvasFeatures,
usingA2aService,
usingA2uiRenderers,
usingDefaultSanitizerMarkdownRenderer,
} from '@a2a_chat_canvas/config';
import {
ApplicationConfig,
provideBrowserGlobalErrorListeners,
provideZonelessChangeDetection,
} from '@angular/core';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
import { DEMO_CATALOG } from '../a2ui-catalog/catalog';
import { A2aServiceImpl } from '../services/a2a-service-impl';

export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZonelessChangeDetection(),
provideClientHydration(withEventReplay()),
configureChatCanvasFeatures(
usingA2aService(A2aServiceImpl),
usingA2uiRenderers(DEMO_CATALOG),
usingDefaultSanitizerMarkdownRenderer(),
),
],
};
Loading
Loading