diff --git a/samples/agent/adk/mcp_app_proxy/agent.py b/samples/agent/adk/mcp_app_proxy/agent.py
index ac06c6852..0fa60329b 100644
--- a/samples/agent/adk/mcp_app_proxy/agent.py
+++ b/samples/agent/adk/mcp_app_proxy/agent.py
@@ -28,7 +28,7 @@
from google.adk.sessions import InMemorySessionService
from google.genai import types
from pydantic import PrivateAttr
-from tools import get_calculator_app, calculate_via_mcp, get_pong_app_a2ui_json
+from tools import get_calculator_app, calculate_via_mcp, get_pong_app_a2ui_json, score_update
from agent_executor import get_a2ui_enabled, get_a2ui_catalog, get_a2ui_examples
logger = logging.getLogger(__name__)
@@ -160,6 +160,13 @@ def _build_agent_card(self) -> AgentCard:
tags=["html", "app", "demo", "tool"],
examples=["open pong", "show pong"],
),
+ AgentSkill(
+ id="score_update",
+ name="Score Update",
+ description="Updates the score for Pong game.",
+ tags=["pong", "score", "tool"],
+ examples=[],
+ ),
],
)
@@ -194,7 +201,12 @@ def _build_llm_agent(
name=self._agent_name,
description="An agent that provides access to MCP Apps.",
instruction=instruction,
- tools=[get_calculator_app, calculate_via_mcp, get_pong_app_a2ui_json],
+ tools=[
+ get_calculator_app,
+ calculate_via_mcp,
+ get_pong_app_a2ui_json,
+ score_update,
+ ],
planner=BuiltInPlanner(
thinking_config=types.ThinkingConfig(
include_thoughts=True,
diff --git a/samples/agent/adk/mcp_app_proxy/catalogs/0.8/mcp_app_catalog.json b/samples/agent/adk/mcp_app_proxy/catalogs/0.8/mcp_app_catalog.json
index 07aad50cc..788e37fa7 100644
--- a/samples/agent/adk/mcp_app_proxy/catalogs/0.8/mcp_app_catalog.json
+++ b/samples/agent/adk/mcp_app_proxy/catalogs/0.8/mcp_app_catalog.json
@@ -42,6 +42,58 @@
"required": [
"content"
]
+ },
+ "Column": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "alignment": { "type": "string" },
+ "distribution": { "type": "string" },
+ "children": {
+ "type": "array",
+ "items": { "type": "object" }
+ }
+ }
+ },
+ "PongScoreBoard": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "playerScore": {
+ "type": "object",
+ "description": "Player score value or path.",
+ "additionalProperties": false,
+ "properties": {
+ "literalNumber": { "type": "number" },
+ "literalString": { "type": "string" },
+ "path": { "type": "string" }
+ }
+ },
+ "cpuScore": {
+ "type": "object",
+ "description": "CPU score value or path.",
+ "additionalProperties": false,
+ "properties": {
+ "literalNumber": { "type": "number" },
+ "literalString": { "type": "string" },
+ "path": { "type": "string" }
+ }
+ }
+ }
+ },
+ "PongLayout": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "mcpComponent": {
+ "type": "object",
+ "description": "McpApp component definition."
+ },
+ "scoreboardComponent": {
+ "type": "object",
+ "description": "PongScoreBoard component definition."
+ }
+ }
}
}
}
diff --git a/samples/agent/adk/mcp_app_proxy/catalogs/0.9/mcp_app_catalog.json b/samples/agent/adk/mcp_app_proxy/catalogs/0.9/mcp_app_catalog.json
index 99619e984..7cc1d7f1d 100644
--- a/samples/agent/adk/mcp_app_proxy/catalogs/0.9/mcp_app_catalog.json
+++ b/samples/agent/adk/mcp_app_proxy/catalogs/0.9/mcp_app_catalog.json
@@ -1,6 +1,38 @@
{
"catalogId": "a2ui.org:a2ui/v0.9/mcp_app_catalog.json",
"components": {
+ "Column": {
+ "type": "object",
+ "allOf": [
+ {
+ "$ref": "common_types.json#/$defs/ComponentCommon"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "component": {
+ "const": "Column"
+ },
+ "alignment": {
+ "type": "string"
+ },
+ "distribution": {
+ "type": "string"
+ },
+ "children": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/anyComponent"
+ }
+ }
+ },
+ "required": [
+ "component"
+ ],
+ "additionalProperties": false
+ }
+ ]
+ },
"McpApp": {
"type": "object",
"allOf": [
@@ -36,6 +68,60 @@
"additionalProperties": false
}
]
+ },
+ "PongScoreBoard": {
+ "type": "object",
+ "allOf": [
+ {
+ "$ref": "common_types.json#/$defs/ComponentCommon"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "component": {
+ "const": "PongScoreBoard"
+ },
+ "playerScore": {
+ "$ref": "common_types.json#/$defs/DynamicNumber",
+ "description": "Player score value or path."
+ },
+ "cpuScore": {
+ "$ref": "common_types.json#/$defs/DynamicNumber",
+ "description": "CPU score value or path."
+ }
+ },
+ "required": [
+ "component"
+ ],
+ "additionalProperties": false
+ }
+ ]
+ },
+ "PongLayout": {
+ "type": "object",
+ "allOf": [
+ {
+ "$ref": "common_types.json#/$defs/ComponentCommon"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "component": {
+ "const": "PongLayout"
+ },
+ "mcpComponent": {
+ "$ref": "#/$defs/McpApp"
+ },
+ "scoreboardComponent": {
+ "$ref": "#/$defs/PongScoreBoard"
+ }
+ },
+ "required": [
+ "component"
+ ],
+ "additionalProperties": false
+ }
+ ]
}
},
"$defs": {
@@ -43,6 +129,15 @@
"oneOf": [
{
"$ref": "#/$defs/McpApp"
+ },
+ {
+ "$ref": "#/$defs/PongScoreBoard"
+ },
+ {
+ "$ref": "#/$defs/PongLayout"
+ },
+ {
+ "$ref": "#/$defs/Column"
}
]
}
diff --git a/samples/agent/adk/mcp_app_proxy/pong_app.html b/samples/agent/adk/mcp_app_proxy/pong_app.html
index 21c98112b..255256b83 100644
--- a/samples/agent/adk/mcp_app_proxy/pong_app.html
+++ b/samples/agent/adk/mcp_app_proxy/pong_app.html
@@ -44,79 +44,65 @@
background: var(--bg);
color: var(--text);
height: 100vh;
+ width: 100vw;
+ margin: 0;
display: flex;
flex-direction: column;
- align-items: center;
- justify-content: center;
+ align-items: stretch;
+ justify-content: stretch;
overflow: hidden;
background: radial-gradient(circle at center, #1a1b3a 0%, #0a0b1e 100%);
}
.container {
position: relative;
- background: var(--card-bg);
- backdrop-filter: blur(10px);
- border-radius: 20px;
- padding: 1.5rem;
- border: 1px solid rgba(255, 255, 255, 0.1);
- box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
+ background: transparent;
display: flex;
flex-direction: column;
gap: 1rem;
- align-items: center;
- width: 95%;
- height: 95%;
- max-width: 800px;
- max-height: 95vh;
- margin: auto;
+ align-items: stretch;
+ flex: 1;
+ min-height: 0;
+ padding: 1.5rem;
+ box-sizing: border-box;
}
.header {
text-align: center;
+ position: relative;
}
- h1 {
- font-family: 'Space Grotesk', sans-serif;
- font-size: 2.5rem;
- font-weight: 700;
- background: linear-gradient(90deg, var(--primary), var(--secondary));
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- margin-bottom: 0.5rem;
- letter-spacing: -1px;
- }
-
- .score-board {
- display: flex;
- gap: 3rem;
- font-size: 1.5rem;
- font-weight: 600;
- margin-bottom: 0.5rem;
- }
-
- .score-item {
- display: flex;
- flex-direction: column;
+ .mcp-badge {
+ display: inline-flex;
align-items: center;
- }
-
- .score-label {
- font-size: 0.75rem;
+ justify-content: center;
+ gap: 4px;
+ background: rgba(255, 0, 255, 0.15);
+ color: #ff88ff;
+ border: 1px solid rgba(255, 0, 255, 0.4);
+ font-size: 0.65rem;
+ padding: 0.25rem 0.75rem;
+ border-radius: 12px;
text-transform: uppercase;
- letter-spacing: 2px;
- opacity: 0.6;
- margin-bottom: 0.25rem;
+ letter-spacing: 0.5px;
+ font-weight: 600;
+ white-space: nowrap;
+ margin-bottom: 0.5rem;
}
canvas {
+ position: absolute;
+ top: 0;
+ left: 0;
background: #000;
border-radius: 12px;
border: 2px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 0 30px rgba(0, 0, 0, 0.3);
cursor: none;
- max-width: 100%;
- height: auto;
- aspect-ratio: 3 / 2;
+ width: 100%;
+ height: 100%;
+ display: block;
+ box-sizing: border-box;
}
.controls {
@@ -211,6 +197,67 @@
.blob-1 { top: -100px; left: -100px; background: var(--primary); }
.blob-2 { bottom: -100px; right: -100px; background: var(--secondary); }
+
@@ -218,17 +265,7 @@
@@ -246,12 +283,36 @@
console.error("Failed to send score update:", e)).finally(() => unpauseGame());
+ }
+
+ function unpauseGame() {
+ isPaused = false;
+ hideOverlay();
+ if (isRunning) {
+ document.getElementById('pause-btn').textContent = "Pause";
+ }
+ }
+
+
+
function render() {
drawRect(0, 0, canvas.width, canvas.height, "#000");
drawNet();
@@ -462,17 +543,12 @@ {
const listener = (event) => {
+ if (event.origin !== PARENT_ORIGIN) {
+ console.warn('Message from untrusted origin ignored:', event.origin);
+ return;
+ }
if (event.data?.id === id) {
window.removeEventListener('message', listener);
if (event.data?.result) {
@@ -269,17 +274,21 @@
}
function sendNotification(method, params) {
- window.parent.postMessage({ jsonrpc: "2.0", method, params }, '*');
+ window.parent.postMessage({ jsonrpc: "2.0", method, params }, PARENT_ORIGIN);
}
window.addEventListener('message', (event) => {
+ if (event.origin !== PARENT_ORIGIN) {
+ console.warn('Message from untrusted origin ignored:', event.origin);
+ return;
+ }
const data = event.data;
if (data?.method === 'ping') {
// The host sends a 'ping' to check if the app is responsive (liveness check).
// Since this example does not use an MCP SDK, we must manually reply with an empty result
// to acknowledge the ping and keep the connection alive.
if (data.id) {
- window.parent.postMessage({ jsonrpc: "2.0", id: data.id, result: {} }, '*');
+ window.parent.postMessage({ jsonrpc: "2.0", id: data.id, result: {} }, PARENT_ORIGIN);
}
}
});
diff --git a/samples/client/angular/projects/a2a-chat-canvas/src/lib/services/chat-service.ts b/samples/client/angular/projects/a2a-chat-canvas/src/lib/services/chat-service.ts
index d77e02440..9031c98c3 100644
--- a/samples/client/angular/projects/a2a-chat-canvas/src/lib/services/chat-service.ts
+++ b/samples/client/angular/projects/a2a-chat-canvas/src/lib/services/chat-service.ts
@@ -76,8 +76,9 @@ export class ChatService {
this.a2uiMessageProcessor.events.subscribe(async (event: DispatchedEvent) => {
try {
// TODO: Replace this with a more robust event handling mechanism.
- // Currently, it just sends the event message back to the agent.
- await this.sendMessage(JSON.stringify(event.message));
+ // Send A2UI actions silently if requested from the action context
+ const isSilent = Boolean(event.message.userAction?.context?.['silent']);
+ await this.sendMessage(JSON.stringify(event.message), isSilent);
event.completion.next([]);
event.completion.complete();
} catch (err) {
@@ -92,8 +93,10 @@ export class ChatService {
*
* @param text The text message to send.
*/
- async sendMessage(text: string) {
- this.addUserAndPendingAgentMessages(text);
+ async sendMessage(text: string, silent: boolean = false) {
+ if (!silent) {
+ this.addUserAndPendingAgentMessages(text);
+ }
this.isA2aStreamOpen.set(true);
try {
@@ -102,9 +105,9 @@ export class ChatService {
[{ kind: 'text', text }],
this.abortController.signal,
);
- this.handleSuccess(a2aResponse);
+ this.handleSuccess(a2aResponse, silent);
} catch (error) {
- this.handleError(error);
+ this.handleError(error, silent);
} finally {
this.isA2aStreamOpen.set(false);
this.abortController = null;
@@ -140,19 +143,21 @@ export class ChatService {
*
* @param response The success response from the A2A service.
*/
- private handleSuccess(response: SendMessageSuccessResponse) {
+ private handleSuccess(response: SendMessageSuccessResponse, silent: boolean = false) {
const agentResponseParts = extractA2aPartsFromResponse(response);
const newContents = agentResponseParts.map(
(part): UiMessageContent => convertPartToUiMessageContent(part, this.partResolvers),
);
- this.updateLastMessage((msg) => ({
- ...msg,
- role: this.createRole(response),
- contents: [...msg.contents, ...newContents],
- status: 'completed',
- lastUpdated: new Date().toISOString(),
- }));
+ if (!silent) {
+ this.updateLastMessage((msg) => ({
+ ...msg,
+ role: this.createRole(response),
+ contents: [...msg.contents, ...newContents],
+ status: 'completed',
+ lastUpdated: new Date().toISOString(),
+ }));
+ }
// Let A2UI Renderer process the A2UI data parts in agent response.
this.a2uiMessageProcessor.processMessages(extractA2uiDataParts(agentResponseParts));
@@ -165,7 +170,12 @@ export class ChatService {
*
* @param error The error object or message.
*/
- private handleError(error: unknown) {
+ private handleError(error: unknown, silent: boolean = false) {
+ if (silent) {
+ console.error('Silent message send failed:', error);
+ return;
+ }
+
let errorMessage = 'Something went wrong: ' + error;
if (error instanceof Error && error.name === 'AbortError') {
errorMessage = 'You cancelled the response.';
diff --git a/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/catalog.ts b/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/catalog.ts
index 89eb8f792..82f58fb61 100644
--- a/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/catalog.ts
+++ b/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/catalog.ts
@@ -28,4 +28,47 @@ export const DEMO_CATALOG = {
inputBinding('title', () => ('title' in properties && properties['title']) || undefined),
],
},
+ PongScoreBoard: {
+ type: () => import('./pong-scoreboard').then((r) => r.PongScoreBoard),
+ bindings: ({ properties }) => [
+ inputBinding(
+ 'playerScore',
+ () => ('playerScore' in properties && properties['playerScore']) || undefined,
+ ),
+ inputBinding(
+ 'cpuScore',
+ () => ('cpuScore' in properties && properties['cpuScore']) || undefined,
+ ),
+ ],
+ },
+ PongLayout: {
+ type: () => import('./pong-layout').then((r) => r.PongLayout),
+ bindings: ({ properties }) => [
+ inputBinding(
+ 'mcpComponent',
+ () => ('mcpComponent' in properties && properties['mcpComponent']) || undefined,
+ ),
+ inputBinding(
+ 'scoreboardComponent',
+ () => ('scoreboardComponent' in properties && properties['scoreboardComponent']) || undefined,
+ ),
+ ],
+ },
+ Column: {
+ type: () => import('@a2ui/angular').then((r) => r.Column),
+ bindings: ({ properties }) => [
+ inputBinding(
+ 'alignment',
+ () => ('alignment' in properties && properties['alignment']) || undefined,
+ ),
+ inputBinding(
+ 'distribution',
+ () => ('distribution' in properties && properties['distribution']) || undefined,
+ ),
+ inputBinding(
+ 'children',
+ () => ('children' in properties && properties['children']) || undefined,
+ ),
+ ],
+ },
} as Catalog;
diff --git a/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/pong-layout.ts b/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/pong-layout.ts
new file mode 100644
index 000000000..c7a2ff7c2
--- /dev/null
+++ b/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/pong-layout.ts
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2026 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 { DynamicComponent } from '../../../../projects/lib/src/public-api';
+import { Renderer } from '../../../../projects/lib/src/v0_8/rendering/renderer';
+import { Component, input } from '@angular/core';
+
+@Component({
+ selector: 'a2ui-pong-layout',
+ standalone: true,
+ imports: [Renderer],
+ template: `
+
+ `,
+})
+export class PongLayout extends DynamicComponent {
+ readonly mcpComponent = input(null);
+ readonly scoreboardComponent = input(null);
+}
diff --git a/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/pong-scoreboard.ts b/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/pong-scoreboard.ts
new file mode 100644
index 000000000..34bd0f9fa
--- /dev/null
+++ b/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/pong-scoreboard.ts
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2026 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 { DynamicComponent } from '@a2ui/angular';
+import * as Primitives from '@a2ui/web_core/types/primitives';
+import * as Types from '@a2ui/web_core/types/types';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ computed,
+ input,
+} from '@angular/core';
+
+@Component({
+ selector: 'a2ui-pong-scoreboard',
+ standalone: true,
+ imports: [],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ styles: `
+ :host {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 1.5rem;
+ padding: 1.5rem;
+ color: #202124;
+ background: #ffffff;
+ height: 100%;
+ width: 100%;
+ position: relative;
+ font-family: 'Google Sans', 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
+ box-sizing: border-box;
+ }
+ .a2ui-badge {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ position: absolute;
+ top: 1rem;
+ left: 50%;
+ transform: translateX(-50%);
+ background: #e8eaed;
+ color: #5f6368;
+ font-size: 0.65rem;
+ padding: 0.25rem 0.75rem;
+ border-radius: 12px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ font-weight: 600;
+ white-space: nowrap;
+ border: 1px solid #dadce0;
+ }
+ .a2ui-icon {
+ font-size: 0.8rem;
+ color: #1a73e8;
+ }
+ .scores-wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+ width: 100%;
+ margin-top: 1.5rem;
+ }
+ .score-card {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ background: #f8f9fa;
+ border-radius: 12px;
+ padding: 1rem;
+ border: 1px solid #e8eaed;
+ transition: all 0.2s ease;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.05);
+ width: 100%;
+ box-sizing: border-box;
+ }
+ .score-card:hover {
+ box-shadow: 0 4px 6px rgba(0,0,0,0.08);
+ transform: translateY(-2px);
+ border-color: #d2e3fc;
+ }
+ .player-card {
+ border-bottom: 3px solid #1a73e8;
+ }
+ .cpu-card {
+ border-bottom: 3px solid #ea4335;
+ }
+ .score-label {
+ font-size: 0.8rem;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ color: #5f6368;
+ margin-bottom: 0.5rem;
+ font-weight: 600;
+ }
+ .score-value {
+ font-size: 3.5rem;
+ font-weight: 700;
+ color: #202124;
+ line-height: 1;
+ }
+ `,
+ template: `
+
+ ✦ A2UI Native
+
+
+
+ Player
+ {{ resolvedPlayerScore() }}
+
+
+ CPU
+ {{ resolvedCpuScore() }}
+
+
+ `,
+})
+export class PongScoreBoard
+ extends DynamicComponent
+{
+ readonly playerScore = input();
+ protected readonly resolvedPlayerScore = computed(() =>
+ super.resolvePrimitive(this.playerScore() ?? null) ?? 0
+ );
+
+ readonly cpuScore = input();
+ protected readonly resolvedCpuScore = computed(() =>
+ super.resolvePrimitive(this.cpuScore() ?? null) ?? 0
+ );
+}