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 @@
-

NEON PONG

-
-
- Player - 0 -
-
- CPU - 0 -
-
+
🔌 Embedded MCP App
@@ -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 + ); +}