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
12 changes: 11 additions & 1 deletion samples/community/agent/adk/mcp_app_proxy/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,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_mcp_app_json, get_pong_app_web_frame_json, commentate_pong_game
from tools import get_calculator_app, calculate_via_mcp, get_pong_mcp_app_json, get_pong_app_web_frame_json, get_pong_app_web_frame_srcdoc_json, commentate_pong_game
from agent_executor import get_a2ui_enabled, get_a2ui_catalog, get_a2ui_examples

logger = logging.getLogger(__name__)
Expand All @@ -40,6 +40,7 @@
When the user asks for the calculator, you MUST call the `get_calculator_app` tool.
When the user asks for Pong with MCP Apps, you MUST call the `get_pong_mcp_app_json` tool.
When the user asks for Pong with WebApp URL, you MUST call the `get_pong_app_web_frame_json` tool.
When the user asks for Pong with WebApp Srcdoc, you MUST call the `get_pong_app_web_frame_srcdoc_json` tool.

IMPORTANT: Do NOT attempt to construct the JSON manually. The tools handle it automatically.

Expand All @@ -53,6 +54,7 @@
- If User asks for calculator: Call `get_calculator_app`.
- If User asks for Pong with MCP Apps: Call `get_pong_mcp_app_json`.
- If User asks for Pong with WebApp URL: Call `get_pong_app_web_frame_json`.
- If User asks for Pong with WebApp Srcdoc: Call `get_pong_app_web_frame_srcdoc_json`.
- If User interacts with the calculator (ACTION: calculate): Extract 'operation', 'a', and 'b' from the event context and call `calculate_via_mcp`. Return the result to the user.
- If you receive a `"commentate_pong"` action: Call `commentate_pong_game` with `"game_event"` from `"context" -> "game_event"`. Do not generate text responses; only call the tool.
"""
Expand Down Expand Up @@ -178,6 +180,13 @@ def _build_agent_card(self) -> AgentCard:
tags=["html", "app", "demo", "tool"],
examples=["open pong with webapp url"],
),
AgentSkill(
id="open_pong_web_frame_srcdoc",
name="Open Pong with WebApp Srcdoc",
description="Opens Pong using the new WebAppFrame Srcdoc method.",
tags=["html", "app", "demo", "tool"],
examples=["open pong with webapp srcdoc"],
),
],
)

Expand Down Expand Up @@ -217,6 +226,7 @@ def _build_llm_agent(
calculate_via_mcp,
get_pong_mcp_app_json,
get_pong_app_web_frame_json,
get_pong_app_web_frame_srcdoc_json,
commentate_pong_game,
],
planner=BuiltInPlanner(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,65 @@
],
"unevaluatedProperties": false
},
"WebAppFrameSrcdoc": {
"type": "object",
"allOf": [
{

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please diff with the iframe sample I had for contact finder in case we have any gaps. We can replace that iframe component with the security approved one then.

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.

I can check once the old iframe sample is revived.
I assume it is using v0.8. Would be best if we can also add some of the feature updates to leverage the v0.9 features.

"$ref": "common_types.json#/$defs/ComponentCommon"
},
{
"type": "object",
"properties": {
"component": {
"const": "WebAppFrameSrcdoc",
"description": "The component type identifier."
},
"htmlContent": {
"type": "string",
"description": "The raw HTML string to render via srcdoc."
},
"height": {
"$ref": "common_types.json#/$defs/DynamicNumber",
"description": "The height of the iframe in pixels."
},
"allowedEvents": {
"type": "object",
"description": "A map of authorized action names to JSON Schemas defining the expected data payload.",
"additionalProperties": {"type": "object"}
},
"allowedFunctions": {
"type": "object",
"description": "A map of authorized host client functions to JSON Schemas defining their expected arguments.",
"additionalProperties": {"type": "object"}
},
"mutableData": {
"type": "object",
"description": "A map of data model keys that the embedded application is authorized to mutate in the parent A2UI Data Model, mapped to JSON Schemas defining their allowed values.",
"additionalProperties": {"type": "object"}
},
"config": {
"type": "object",
"description": "A dictionary of static key-value initialization properties passed directly to the embedded application without reactive data model binding."
},
"data": {
"type": "object",
"description": "Data binding configuration for the component.",
"properties": {
"paths": {
"type": "object",
"description": "A dictionary mapping custom state keys to distinct JSON Pointer paths in the data model.",
"additionalProperties": {"type": "string"}
}
},
"required": ["paths"],
"additionalProperties": false
}
},
"required": ["component", "htmlContent"]
}
],
"unevaluatedProperties": false
},
"PongScoreBoard": {
"type": "object",
"allOf": [
Expand Down Expand Up @@ -218,6 +277,9 @@
},
{
"$ref": "#/components/WebAppFrameUrl"
},
{
"$ref": "#/components/WebAppFrameSrcdoc"
}
]
}
Expand Down
103 changes: 103 additions & 0 deletions samples/community/agent/adk/mcp_app_proxy/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
# 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 asyncio
import os
import urllib.parse
import urllib.request
import traceback
import logging

Expand All @@ -28,6 +30,12 @@
PONG_CURRENT_SCORE = {"player": 0, "cpu": 0}


def _fetch_url(url: str, headers: dict[str, str], timeout: float = 2.0) -> str:
req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req, timeout=timeout) as response:
return response.read().decode("utf-8")


# Define get_calculator_app tool in a way that the LlmAgent can use.
async def get_calculator_app(tool_context: ToolContext):
"""Fetches the calculator app."""
Expand Down Expand Up @@ -286,6 +294,101 @@ async def get_pong_app_web_frame_json(tool_context: ToolContext):
return {"validated_a2ui_json": messages}


async def get_pong_app_web_frame_srcdoc_json(tool_context: ToolContext):
"""Fetches the Pong game app using the WebAppFrameSrcdoc component."""

# Reset score on reload
global PONG_CURRENT_SCORE
PONG_CURRENT_SCORE = {"player": 0, "cpu": 0}

remote_url = os.getenv(
"PONG_SERVER_URL", "http://localhost:8081/pong_app_web_frame_srcdoc.html"
)
loop = asyncio.get_running_loop()
try:
html_content = await loop.run_in_executor(
None,
_fetch_url,
remote_url,
{"User-Agent": "A2UI-Agent"},
)
except Exception as e:
logger.error(f"Could not fetch remote pong app from {remote_url}: {e}.")
return {"error": f"Could not fetch pong app from remote server ({e})"}

messages = [
{
"version": "v0.9",
"createSurface": {
"surfaceId": PONG_SURFACE_ID,
"catalogId": (
"https://a2ui.org/samples/community/agent/adk/mcp_app_proxy/catalogs/0.9/mcp_app_catalog.json"
),
},
},
{
"version": "v0.9",
"updateDataModel": {
"surfaceId": PONG_SURFACE_ID,
"path": "/",
"value": {
"pong_state": {
"player_score": PONG_CURRENT_SCORE["player"],
"cpu_score": PONG_CURRENT_SCORE["cpu"],
"commentary": "Let the match begin!",
}
},
},
},
{
"version": "v0.9",
"updateComponents": {
"surfaceId": PONG_SURFACE_ID,
"components": [
{
"id": "root",
"component": "PongLayout",
"mcpComponent": "web_frame_app_root",
"scoreboardComponent": "scoreboard_root",
},
{
"id": "web_frame_app_root",
"component": "WebAppFrameSrcdoc",
"htmlContent": html_content,
"allowedEvents": {
"commentate_pong": {
"type": "object",
"properties": {
"game_event": {"type": "string"},
"silent": {"type": "boolean"},
},
}
},
"allowedFunctions": {
"showWinnerModal": {
"type": "object",
"properties": {"winner": {"type": "string"}},
}
},
"mutableData": {"state": {}},
"config": {"matchingScore": 5},
"data": {"paths": {"state": "/pong_state"}},
},
{
"id": "scoreboard_root",
"component": "PongScoreBoard",
"playerScore": {"path": "/pong_state/player_score"},
"cpuScore": {"path": "/pong_state/cpu_score"},
"commentary": {"path": "/pong_state/commentary"},
},
],
},
},
]
tool_context.actions.skip_summarization = True
return {"validated_a2ui_json": messages}


async def commentate_pong_game(tool_context: ToolContext, game_event: str):
"""Generates a witty neon-themed sports commentary or lighthearted trash talk comment based on the game event description, and applies it to the game scoreboard.

Expand Down
1 change: 1 addition & 0 deletions samples/community/client/angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"build": "ng build a2a-chat-canvas && ng build orchestrator && ng build mcp_calculator",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test": "echo \"Workspace has no tests.\" && exit 0",
"clean": "rm -rf dist .tsbuildinfo .wireit out-tsc",
"format": "prettier --write .",
"format:check": "prettier --check ."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {PongScoreBoard} from './pong-scoreboard';
import {PongLayout} from './pong-layout';
import {Column} from '@a2ui/angular';
import {WebAppFrameUrl} from './web-app-frame-url';
import {WebAppFrameSrcdoc} from './web-app-frame-srcdoc';

/**
* The catalog ID for the MCP App catalog.
Expand Down Expand Up @@ -56,6 +57,18 @@ const PongLayoutSchema = z.object({

const WebAppFrameUrlSchema = z.object({
url: DynamicStringSchema,
config: z.record(z.any()).optional(),
data: DynamicValueSchema.optional(),
height: DynamicNumberSchema.optional(),
allowedEvents: z.record(z.any()).optional(),
allowedFunctions: z.record(z.any()).optional(),
mutableData: z.record(z.any()).optional(),
disableSchemaValidation: z.boolean().optional(),
});

const WebAppFrameSrcdocSchema = z.object({
htmlContent: z.string(),
config: z.record(z.any()).optional(),
data: DynamicValueSchema.optional(),
height: DynamicNumberSchema.optional(),
allowedEvents: z.record(z.any()).optional(),
Expand Down Expand Up @@ -139,6 +152,7 @@ export const DEMO_CATALOG = new Catalog(
{name: 'PongScoreBoard', component: PongScoreBoard, schema: PongScoreBoardSchema},
{name: 'PongLayout', component: PongLayout, schema: PongLayoutSchema},
{name: 'WebAppFrameUrl', component: WebAppFrameUrl, schema: WebAppFrameUrlSchema},
{name: 'WebAppFrameSrcdoc', component: WebAppFrameSrcdoc, schema: WebAppFrameSrcdocSchema},
// Column should use ColumnApi.schema from @a2ui/web_core, but it is not currently
// exported by the version of @a2ui/web_core resolved in this community sample.
// We use z.any() to avoid duplicating the schema definition here.
Expand Down
Loading
Loading