Skip to content

feat: align MCP app v0.9 bridge, specs, pong improvements, and data d…#1945

Merged
sugoi-yuzuru merged 8 commits into
mainfrom
spec_align
Jul 14, 2026
Merged

feat: align MCP app v0.9 bridge, specs, pong improvements, and data d…#1945
sugoi-yuzuru merged 8 commits into
mainfrom
spec_align

Conversation

@sugoi-yuzuru

Copy link
Copy Markdown
Collaborator

Description

Overview

This pull request introduces the MCP Apps component specification for A2UI V0.9, and updates the McpApp component and the Pong game demo to the introduced A2UI v0.9 specification. The changes include adding the component specification document, implementing host-guest message routing, updating the Pong application logic, and preventing cyclic updates.

See the updated Pong Demo here https://screencast.googleplex.com/cast/NjE3MDM1NzE3MzA1OTU4NHxkYTRhMjhjNy01Yw

Key changes

  • Added specification: Added mcp-apps-component_spec.md to define security boundaries, message schemas, and catalog properties for the McpApp component.

  • Component properties: Updated McpAppSchema in catalog.ts to replace the older content field with htmlContent (which was conflicting with a reserved property name in A2UI v0.9), and added allowedFunctions and data properties.

  • Double-iframe host logic: Updated mcp-app.ts to process sizing requests, handle client-side function invocations, and route data model updates.

  • Deduplication: Added structural equality validation using stringified JSON comparisons to break infinite state update loops between host and the embedded app.

  • Scoreboard commentary: Updated pong-scoreboard.ts to display generated game commentary to demonstrate the Allowed Tool Calls from the embedded MCP App.

  • Score tracking: Modified pong_app.html to run score tracking using the A2UI V0.9's Tow-way data binding feature (instead of ToolCall routing we did in A2UI V0.8).

  • Game winner: Implemented a UI overlay that announces the winner of the game in the A2UI catalog to demonstrate the Allowed Function Calls from the embedded MCP App. This is integration with the new FunctionCall feature on A2UI V0.9.

The sections below describe how the specifications are demonstrated in the Pong game demo.

Dynamic resizing
The specification allows sandboxed applications to request window dimension changes. The host implements layout constraints including clamping, throttling, and change thresholds.

In the demo, once
pong_app.html
finishes initializing and completes the handshake, it transmits a ui/notifications/size-change request to resize the wrapper to 728 by 502 pixels. The host component receives this and resizes the iframe and parent container styles. This keeps the canvas fully visible without displaying scrollbars.

Two-way local data binding
The specification defines data synchronization between the sandboxed application and the host data model. This path is configured via data.path in the catalog.

In the demo, this is shown in two ways:

  • Upward state updates; When a player or the CPU scores, pong_app.html updates its local score values and posts a ui/notifications/data-model-change message containing the new scores. The host updates /pong_state on the client, which updates the native pong-scoreboard.ts component. Because this update occurs client-side, score displays update immediately without remote agent roundtrips.
  • Downward state updates; When a match ends, the user can click "Play Again" in the victory modal. This triggers host-side logic that resets /pong_state scores to zero. The host detects this change and pushes a ui/notifications/data-model-update notification to the iframe. The game script processes this update to clear its internal score variables, reset the ball, and resume play.

Infinite event loop prevention
Both the host component and the game script check incoming values against the last processed state. If the stringified JSON matches, the update is dropped. This prevents the host and guest from continuously re-triggering updates.

Local client-side function execution
The specification allows the sandboxed application to trigger pre-registered host functions if permitted by allowedFunctions.

In the demo, when a score reaches three points, the game script calls the host function showWinnerModal. The host validates permission and evaluates the function. The function creates and displays an HTML dialog stating the winner. Clicking the restart button inside the modal updates the host data model to reset the scores.

Tool call routing
The specification permits sandboxed applications to request remote tool executions defined by the agent.

In the demo, whenever a point is scored or the match restarts, the game script requests the commentate_pong tool. The host forwards this action to the agent backend. The agent calls the Gemini model to produce a short comment under 15 words. The agent then writes the comment back to /pong_state/commentary, which is updated on the scoreboard.

Pre-launch Checklist

One time:

For this PR:

  • I have added updates to the CHANGELOG.
  • I updated/added relevant documentation.
  • My code changes (if any) have tests.
  • If my branch is on fork, I have verified that scripts/e2e_test.sh passes.

If you need help, consider asking for advice on the discussion board.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request updates the MCP App Proxy community sample to support A2UI v0.9 features, including two-way local data binding, local client-side function execution, and dynamic resizing. It replaces the local score update tool with an AI-generated live commentary tool (commentate_pong_game) and adds a winner modal dialog. Key feedback points out a potential ReferenceError during early initialization in pong_app.html, state overwriting in catalog.ts that deletes commentary, missing parent width updates in mcp-app.ts causing horizontal clipping, redundant inline imports, and a potential AttributeError in tools.py if the LLM response is empty.

Comment thread samples/community/agent/adk/mcp_app_proxy/pong_app.html Outdated
Comment thread samples/community/agent/adk/mcp_app_proxy/tools.py Outdated
Comment thread samples/community/agent/adk/mcp_app_proxy/tools.py Outdated
…eduplication

- docs: add component specification for McpApp to the A2UI catalog

- feat: add MCP app v0.9 bridge support with dynamic resizing, local function execution, and two-way data binding

- feat: fix pong app layout by enforcing fixed dimensions and updating container constraints

- feat: add AI-generated live commentary to the Pong game via a new tool and UI integration

- fix: prevent circular data model updates by implementing structural equality deduplication
@sugoi-yuzuru
sugoi-yuzuru enabled auto-merge (squash) July 8, 2026 21:29

@jacobsimionato jacobsimionato left a comment

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.

Hey this looks great to me in general - most of my comments are about the way it's expressed and more subtle technical details.

Comment thread samples/community/agent/adk/mcp_app_proxy/catalogs/0.9/mcp_app_catalog.json Outdated
**Host action**
The host checks if the requested tool name is in the component's `allowedTools` list. If it is authorized, the host dispatches the tool call as an A2UI action to the agent backend. If not, it rejects the tool call and returns a JSON-RPC error response.

### B. Reactive state synchronization (`ui/notifications/data-model-change`)

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.

I wonder if this should include a subpath param. There is only one data model binding key, which is used to propagate state from host -> app and app -> host, so if both are overwriting the entire state, I think race conditions and clobbering could occur seeing as there is an async jump over the sandbox boundary.

E.g.

  1. Establish MCP calculator app bound to /calculator_state
  2. Server sends UpdateDataModel message to update /calculator_state/mode to scientific
  3. Calculator responds to button press, sending data-model-change message to update /calculator_state/expression_string to 3. However, it actually has to copy the entire existing content of /calculator_state to update one value, e.g. it is setting it to {"mode": "basic", "expression_string": "3"}.
  4. Host receives server update, updating /calculator_state/mode to scientific. It sends ui/notifications/data-model-update to calculator with the updated state.
  5. Host receives data-model-change setting /calculator_state back to {"mode": "basic", "expression_string": "3"}, clobbering its state
  6. Calculator receives ui/notifications/data-model-update with {"mode": "scientific", "expression_string": ""}, clobbering its state

I dunno, WDYT? Maybe I'm nit picking! Another option would be to have two params - one intended for host->app state and another for app->host.

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 didn't think about that so thank you for pointing me to this.

I have extended to accept a more surgical data update by targeting a subpath for the data.
It adds a bit of complexity to the deep-equality checks for preventing loop, but this is contained within the MCP App component.

I have also added a toggle switch to prevent echoing when the data updates from App -> Host, and the Host -> App broadcast is auto-triggered, that the event (downstream) propagation is terminated if the data update was originated form the App.

See cc9b4f6 commit.

```

**Host action**
The host checks if the requested tool name is in the component's `allowedTools` list. If it is authorized, the host dispatches the tool call as an A2UI action to the agent backend. If not, it rejects the tool call and returns a JSON-RPC error response.

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.

(sorry if I asked this before) is this necessary given that these RPCs will be forwarded to the host anyway, and it can always reject them there?

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.

It mostly depends on how much trust-worthy the embedded app is.

From the back-end's POV, the request will be coming from the host application and will assume the source is trust-worthy. However, if the request is originating from the embedded app, there may be some calls that the backend might not want to blindly execute (e.g., deleteAccount or readFileFromLocal etc)

The host receives the `htmlContent` property from the catalog definition:

1. It extracts the raw HTML string.
2. If the string starts with `url_encoded:`, it decodes the content using `decodeURIComponent`.

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.

Is this a standard approach? It seems a bit magical and confusing to me. Could we just support either always URL encoded, or never URL encoded? Or even have alternative input properties for each option (though that would require them to be optional I know).

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.

So this is actually tricky.

MCP Apps naturally are served from MCP server. As a result, native MCP Apps are fetched from a client directly from a MCP server's resource endpoint (or Tool endpoint). This means the raw HTML doesn't go through LLM's context which is great.

On the other hand, when MCP Apps are inserted within an A2UI payload, they can be included in the Agent's LLM dialog. In such case, the raw HTML is terrible due to the HTML tags and excessive new lines. URL encoding helps make them into a single "mystery string" to maintain its composure.

I may be able to study a bit more from Liad's change and more examples about MCP Apps on how I might be able to get the HTML blob out-of-LLM-context, but as of now, this was the solution I had in place.

@sugoi-yuzuru sugoi-yuzuru Jul 9, 2026

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.

Assuming we want to make A2UI as simple as Client UI <-> A2UI Agent, I need to figure out how I might be able to make MCP Apps A2UI component to asynchronously fetch MCP App resource from A2UI Agent without contaminating the LLM Context.

I will work on this as a follow up since it is pretty critical.

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.

Hey I agree this is a critical problem!

The architecture I was actually hoping to get to with A2UI is to have a transformer between the actual persisted message context, and any LLM inferences, so that we can store the HTML string in its most basic form, but then omit context as necessary to hide it from the LLM, without needing to introduce a more complex RPC protocol with async fetching etc.

There are multiple reasons we might want to transform A2UI content between the message history and LLM:

  • To expose A2UI via a more token efficient format - see https://docs.google.com/document/d/1mH2DJ_jZZA2vZ9EQlWaU4v8_xCAcmqrox_LlElmJ3ws/edit?tab=t.0
  • For surfaces that have been updated incrementally, to flatten them to a snapshot of current state
  • To include ephemeral state of surfaces pulled from the client
  • To omit aspects of the A2UI content which are not relevant to future inferences, to save tokens. E.g. protocol details like catalog ID, surface ID, component IDs even (perhaps we could use a nested format for reading).

Seeing as we need all this translation stuff anyway, maybe we can have a concept of "plugins" that allow you to add additional catalog-specific transformations, or they can even be included as part of the catalog implementation, or annotated on the catalog API in the future "hiddenFromHistory = true" etc.

WDYT?

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 love the idea of having an API to omit certain parts of A2UI payload from the context history, and very happy to onboard on that!

I think there are ways I can leverage architectures learned from MCP to mitigate the problem for MCP Apps and IFrame, but would be great to have a solution overarching the A2UI as a whole :)

That being said, is there something I can add to the current PR? Might I add a Note or a comment on forthcoming changes that might influence this specification?

…eduplication

- docs: add component specification for McpApp to the A2UI catalog

- feat: add MCP app v0.9 bridge support with dynamic resizing, local function execution, and two-way data binding

- feat: fix pong app layout by enforcing fixed dimensions and updating container constraints

- feat: add AI-generated live commentary to the Pong game via a new tool and UI integration

- fix: prevent circular data model updates by implementing structural equality deduplication
… data, and title while updating the specification documentation.
@sugoi-yuzuru
sugoi-yuzuru merged commit 3f6877b into main Jul 14, 2026
23 checks passed
@sugoi-yuzuru
sugoi-yuzuru deleted the spec_align branch July 14, 2026 21:21
@github-project-automation github-project-automation Bot moved this from Todo to Done in A2UI Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants