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
18 changes: 6 additions & 12 deletions samples/community/agent/adk/mcp_app_proxy/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, score_update
from tools import get_calculator_app, calculate_via_mcp, get_pong_app_a2ui_json, commentate_pong_game
from agent_executor import get_a2ui_enabled, get_a2ui_catalog, get_a2ui_examples

logger = logging.getLogger(__name__)
Expand All @@ -40,16 +40,17 @@

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

When the user interacts with the calculator and issues a `calculate` action, you MUST call the `calculate_via_mcp` tool to perform the calculation via the remote MCP server. Return the resulting number directly as text to the user.
When the user interacts with the Pong game and issues a `score_update` action, you MUST call the `score_update` tool with the scoring player to update the scoreboard.
When the user interacts with the calculator and issues a `calculate` action, you MUST call the `calculate_via_mcp` tool. Return the resulting number directly as text to the user.

When you receive a `"commentate_pong"` action, immediately call `commentate_pong_game` tool with `"game_event"` from `"context" -> "game_event"`. Do not reply with text; only call the tool.
"""

WORKFLOW_DESCRIPTION = """
1. **Analyze Request**:
- If User asks for calculator: Call `get_calculator_app`.
- If User asks for Pong: Call `get_pong_app_a2ui_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 User interacts with the Pong game (ACTION: score_update): Extract 'player' from the event context and call `score_update`.
- 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.
"""

UI_DESCRIPTION = """
Expand Down Expand Up @@ -164,13 +165,6 @@ 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=[],
),
],
)

Expand Down Expand Up @@ -209,7 +203,7 @@ def _build_llm_agent(
get_calculator_app,
calculate_via_mcp,
get_pong_app_a2ui_json,
score_update,
commentate_pong_game,
],
planner=BuiltInPlanner(
thinking_config=types.ThinkingConfig(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "object",
"additionalProperties": false,
"properties": {
"content": {
"htmlContent": {
"type": "object",
"description": "The HTML content of the app.",
"additionalProperties": false,
Expand Down Expand Up @@ -39,7 +39,7 @@
}
}
},
"required": ["content"]
"required": ["htmlContent"]
},
"Column": {
"type": "object",
Expand Down Expand Up @@ -76,6 +76,15 @@
"literalString": {"type": "string"},
"path": {"type": "string"}
}
},
"commentary": {
"type": "object",
"description": "AI Live commentary value or path.",
"additionalProperties": false,
"properties": {
"literalString": {"type": "string"},
"path": {"type": "string"}
}
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"component": {
"const": "McpApp"
},
"content": {
"htmlContent": {
"$ref": "common_types.json#/$defs/DynamicString",
"description": "The HTML content of the app."
},
Expand All @@ -59,9 +59,21 @@
"items": {
"type": "string"
}
},
"allowedFunctions": {
"type": "array",
"description": "List of local function names the app is allowed to call.",
"items": {
"type": "string"
}
},
"data": {
"$ref": "common_types.json#/$defs/DynamicValue",
"description": "The A2UI data model path or value bound to this component for reactive state synchronization."
}
},
"required": ["component", "content"]
"required": ["component", "htmlContent"],
"additionalProperties": false
}
],
"unevaluatedProperties": false
Expand All @@ -85,6 +97,10 @@
"cpuScore": {
"$ref": "common_types.json#/$defs/DynamicNumber",
"description": "CPU score value or path."
},
"commentary": {
"$ref": "common_types.json#/$defs/DynamicString",
"description": "AI Live commentary value or path."
}
},
"required": ["component"]
Expand Down
112 changes: 85 additions & 27 deletions samples/community/agent/adk/mcp_app_proxy/pong_app.html
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@
* and state updates.
*/
let nextId = 1;
let localPlayerScore = 0;
let localCpuScore = 0;

function sendRequest(method, params) {
const id = nextId++;
Expand Down Expand Up @@ -246,11 +248,45 @@
if (data.id) {
window.parent.postMessage({jsonrpc: '2.0', id: data.id, result: {}}, '*');
}
} else if (data === 'unpause' || data?.type === 'unpause') {
if (typeof unpauseGame === 'function') {
unpauseGame();
} else {
console.warn('unpauseGame is not defined yet');
} else if (data?.method === 'ui/notifications/data-model-update') {
// TODO: Refactor message handlers to use `@modelcontextprotocol/ext-apps/app-bridge` App client.
const params = data.params;
const subpath = params?.subpath;
const value = params?.value;

if (subpath) {
if (subpath === '/player_score' && typeof value === 'number') {
localPlayerScore = value;
} else if (subpath === '/cpu_score' && typeof value === 'number') {
localCpuScore = value;
}
} else if (value && typeof value === 'object') {
if (typeof value.player_score === 'number') {
localPlayerScore = value.player_score;
}
if (typeof value.cpu_score === 'number') {
localCpuScore = value.cpu_score;
}
}

// Automatically restart if scores reset to 0
if (localPlayerScore === 0 && localCpuScore === 0) {
if (typeof isPaused !== 'undefined') {
isPaused = false;
}
if (typeof hideOverlay === 'function') {
hideOverlay();
}
if (typeof resetBall === 'function') {
resetBall();
}
sendRequest('tools/call', {
name: 'commentate_pong',
arguments: {
game_event: 'Match started! Current Score: Player 0 - CPU 0.',
silent: true,
},
}).catch(e => console.error('Failed to request commentary:', e));
}
}
});
Expand All @@ -271,6 +307,7 @@
.then(result => {
console.log('Initialized with host:', result);
sendNotification('ui/notifications/initialized', {});
sendNotification('ui/notifications/size-changed', {width: 728, height: 502});
})
.catch(err => {
console.error('Initialization failed:', err);
Expand All @@ -287,14 +324,7 @@
</div>

<div
style="
position: relative;
width: 100%;
display: flex;
justify-content: center;
flex: 1;
min-height: 0;
"
style="position: relative; display: flex; justify-content: center; flex: 1; min-height: 0"
>
<canvas id="pong"></canvas>
<div id="game-overlay">
Expand Down Expand Up @@ -520,22 +550,50 @@
}

function syncScore(player) {
isPaused = true; // Local pause
displayOverlay('SYNCING SCORE...');
if (player === 'player') {
localPlayerScore++;
} else if (player === 'cpu') {
localCpuScore++;
}

sendRequest('tools/call', {
name: 'score_update',
arguments: {player: player, silent: true},
})
.catch(e => console.error('Failed to send score update:', e))
.finally(() => unpauseGame());
}
console.log('Updating score: ', localPlayerScore, localCpuScore);

if (player === 'player') {
sendNotification('ui/notifications/data-model-change', {
subpath: '/player_score',
value: localPlayerScore,
});
} else if (player === 'cpu') {
sendNotification('ui/notifications/data-model-change', {
subpath: '/cpu_score',
value: localCpuScore,
});
}

function unpauseGame() {
isPaused = false;
hideOverlay();
if (isRunning) {
document.getElementById('pause-btn').textContent = 'Pause';
let eventDescription = 'player scored';
if (localPlayerScore >= 3) {
eventDescription = 'player won the match';
} else if (localCpuScore >= 3) {
eventDescription = 'cpu won the match';
} else if (player === 'cpu') {
eventDescription = 'cpu scored';
}

sendRequest('tools/call', {
name: 'commentate_pong',
arguments: {
game_event: `Score: Player ${localPlayerScore} - CPU ${localCpuScore} (${eventDescription}).`,
silent: true,
},
}).catch(e => console.error('Failed to request commentary:', e));

if (localPlayerScore >= 3 || localCpuScore >= 3) {
isPaused = true;
displayOverlay(localPlayerScore >= 3 ? 'YOU WIN!' : 'CPU WINS!');
sendRequest('ui/requests/function-call', {
call: 'showWinnerModal',
args: {winner: localPlayerScore >= 3 ? 'player' : 'cpu'},
}).catch(e => console.error('Failed to trigger showWinnerModal:', e));
}
}

Expand Down
Loading
Loading