Skip to content

Commit 3e431c5

Browse files
committed
Merge remote-tracking branch 'upstream/main' into formatting-tweaks
2 parents cb07e32 + ebb474b commit 3e431c5

109 files changed

Lines changed: 4833 additions & 649 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

agent_sdks/kotlin/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ val copySpecs by tasks.registering(Copy::class) {
7373
from(File(repoRoot, "specification/v0_9/json/common_types.json")) {
7474
into("com/google/a2ui/assets/0.9")
7575
}
76-
from(File(repoRoot, "specification/v0_9/json/basic_catalog.json")) {
77-
into("com/google/a2ui/assets/0.9")
76+
from(File(repoRoot, "specification/v0_9/catalogs/basic/catalog.json")) {
77+
into("com/google/a2ui/assets/0.9/catalogs/basic")
7878
}
7979

8080
into(layout.buildDirectory.dir("generated/resources/specs"))

agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/basic_catalog/BasicCatalogProvider.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,12 @@ class BundledCatalogProvider(private val version: A2uiVersion) : A2uiCatalogProv
3838
override fun load(): JsonObject {
3939
val specMap = BasicCatalog.BASIC_CATALOG_PATHS[version] ?: emptyMap()
4040
val relPath = specMap[A2uiConstants.CATALOG_SCHEMA_KEY] ?: ""
41-
val filename = relPath.substringAfterLast('/')
41+
val filename =
42+
if (version == A2uiVersion.VERSION_0_9) {
43+
relPath.substringAfter("specification/v0_9/")
44+
} else {
45+
relPath.substringAfterLast('/')
46+
}
4247

4348
val resource =
4449
SchemaResourceLoader.loadFromBundledResource(version.value, filename)?.toMutableMap()
@@ -75,7 +80,7 @@ object BasicCatalog {
7580
"specification/v0_8/json/standard_catalog_definition.json"
7681
),
7782
A2uiVersion.VERSION_0_9 to
78-
mapOf(A2uiConstants.CATALOG_SCHEMA_KEY to "specification/v0_9/json/basic_catalog.json"),
83+
mapOf(A2uiConstants.CATALOG_SCHEMA_KEY to "specification/v0_9/catalogs/basic/catalog.json"),
7984
)
8085

8186
/**

agent_sdks/kotlin/src/test/kotlin/com/google/a2ui/core/schema/ValidatorTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ class ValidatorTest {
124124
"""
125125
{
126126
"${"\$schema"}": "https://json-schema.org/draft/2020-12/schema",
127-
"${"\$id"}": "https://a2ui.org/specification/v0_9/basic_catalog.json",
127+
"${"\$id"}": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json",
128128
"catalogId": "basic",
129129
"components": {
130130
"Text": {

agent_sdks/python/tests/integration/verify_load_real.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,9 @@ def verify():
403403
'version': 'v0.9',
404404
'createSurface': {
405405
'surfaceId': 'contact_form_1',
406-
'catalogId': 'https://a2ui.dev/specification/v0_9/basic_catalog.json',
406+
'catalogId': (
407+
'https://a2ui.dev/specification/v0_9/catalogs/basic/catalog.json'
408+
),
407409
'fakeProperty': 'should be allowed',
408410
},
409411
},

agent_sdks/python/tests/schema/test_catalog.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828

2929
def test_catalog_id_property():
30-
catalog_id = "https://a2ui.org/basic_catalog.json"
30+
catalog_id = "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json"
3131
catalog = A2uiCatalog(
3232
version=VERSION_0_8,
3333
name=BASIC_CATALOG_NAME,

agent_sdks/python/tests/schema/test_validator.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,9 @@ def catalog_0_9(self):
130130
}
131131
catalog_schema = {
132132
"$schema": "https://json-schema.org/draft/2020-12/schema",
133-
"$id": "https://a2ui.org/specification/v0_9/basic_catalog.json",
133+
"$id": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json",
134134
"title": "A2UI Basic Catalog",
135-
"catalogId": "https://a2ui.dev/specification/v0_9/basic_catalog.json",
135+
"catalogId": "https://a2ui.dev/specification/v0_9/catalogs/basic/catalog.json",
136136
"components": {
137137
"Text": {
138138
"type": "object",
@@ -449,7 +449,9 @@ def test_pretty_error_messages(self, catalog_0_9):
449449
"version": "v0.9",
450450
"createSurface": {
451451
"surfaceId": "recipe-card",
452-
"catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json",
452+
"catalogId": (
453+
"https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json"
454+
),
453455
},
454456
},
455457
{

docs/assets/recipe_sample.gif

8.43 MB
Loading

docs/guides/a2ui_over_mcp.md

Lines changed: 130 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ cd A2UI/samples/mcp/a2ui-over-mcp-recipe
2828
uv run .
2929
```
3030

31+
### Option A: Interacting via the MCP Inspector
32+
3133
In a separate terminal, launch the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) to interact with the server:
3234

3335
```bash
@@ -38,8 +40,10 @@ In the Inspector:
3840

3941
1. Set **Transport Type** to `SSE`
4042
2. Connect to `http://localhost:8000/sse`
41-
3. Click **List Tools** → you'll see `get_recipe_a2ui`
42-
4. Run the tool → the response contains A2UI JSON that renders a recipe card
43+
3. Click **List Resources** → you'll see "Recipe Form" resource.
44+
4. Read the `a2ui://recipe-form` resource → the resource content is the A2UI JSON that renders the simple form.
45+
5. Click **List Tools** → you'll see `get_recipe_a2ui`
46+
6. Run the tool → the response contains A2UI JSON that renders a recipe card
4347

4448
> NOTE: Note
4549
>
@@ -49,11 +53,57 @@ In the Inspector:
4953
> pip install a2ui-agent-sdk
5054
> ```
5155
56+
### Option B: Running the Recipe Client Web App
57+
58+
For a fully rendered interactive experience that visually demonstrates A2UI over MCP, run the included web application:
59+
60+
1. In a new terminal window, navigate to the client directory:
61+
```bash
62+
cd client
63+
```
64+
2. Install Node.js dependencies:
65+
```bash
66+
npm install
67+
```
68+
3. Start the Vite development server:
69+
```bash
70+
npm run dev
71+
```
72+
4. Open your browser to the URL displayed in your terminal (usually `http://localhost:5173`).
73+
74+
You will see a premium, responsive dual-column interface where the left column renders the Selection Form from MCP Resource (`a2ui://recipe-form`). Picking options and clicking **"Get Recipe"** executes the MCP Tool (`get_recipe_a2ui`), dynamically rendering the returned custom A2UI recipe card in the right column.
75+
76+
![Dynamic Recipe Studio demo showing selection form on the left and dynamic recipe card generation on the right](../assets/recipe_sample.gif)
77+
5278
See all samples at [`samples/mcp/`](https://github.com/google/A2UI/tree/main/samples/mcp).
5379
5480
## How It Works
5581
56-
An MCP server returns A2UI content as **Embedded Resources** inside tool responses. The client detects the `application/json+a2ui` MIME type and routes the payload to an A2UI renderer.
82+
There are two primary ways an MCP server can deliver A2UI content to a client:
83+
84+
1. **Via Reading a Resource (`resources/read`)**: The client reads an MCP resource directly (e.g., `a2ui://recipe-form`). The server returns the A2UI JSON payload directly.
85+
2. **Via Calling a Tool (`tools/call`)**: The client calls an MCP tool (e.g., `get_recipe_a2ui`). The server returns the A2UI JSON payload wrapped as an **Embedded Resource** inside the tool response.
86+
87+
In both cases, the client detects the `application/json+a2ui` MIME type and routes the payload to an A2UI renderer.
88+
89+
> [!IMPORTANT]
90+
> **MIME Type Uniformity**
91+
> Regardless of the delivery channel (whether fetched directly as a Resource or returned inside a Tool's `CallToolResult`), the A2UI JSON payload is always identified by the `application/json+a2ui` MIME type. In Tool responses, the payload must be wrapped inside an `EmbeddedResource` carrying this MIME type. This uniform identification allows client-side middleware to seamlessly intercept and route both static resources and dynamic tool responses to A2UI.
92+
93+
### 1. Resource-based Delivery Flow (`resources/read`)
94+
95+
```
96+
Client → resources/read → MCP Server
97+
98+
Retrieve A2UI JSON
99+
100+
Client ← ResourceContents ← MCP Server
101+
(application/json+a2ui)
102+
103+
A2UI Renderer displays UI
104+
```
105+
106+
### 2. Tool-based Delivery Flow (`tools/call`)
57107
58108
```
59109
Client → tools/call → MCP Server
@@ -68,6 +118,83 @@ Client ← CallToolResult ← MCP Server
68118
A2UI Renderer displays UI
69119
```
70120
121+
## Resources vs. Tools: Separation of Utility Focus
122+
123+
When designing an A2UI integration over MCP, you should choose between **Resources** and **Tools** depending on whether the UI payload is static or dynamic.
124+
125+
### 1. Static UI via MCP Resources (`resources/read`)
126+
127+
For simple, static user interfaces that do not depend on user prompt inputs or conversation history, you should serve A2UI directly as an MCP Resource.
128+
129+
- **Concept**: The client reads a pre-defined A2UI resource using a standard resource URI (e.g., `a2ui://recipe-form`).
130+
- **Use Case**: Ideal for static configuration forms, selection screens, settings dashboards, or stable layouts.
131+
- **Benefit**: Extremely simple to implement, low overhead, and doesn't require the LLM/Agent to make a tool call to fetch the structure.
132+
133+
**Python Server Example:**
134+
135+
```python
136+
@app.list_resources()
137+
async def list_resources() -> list[types.Resource]:
138+
return [
139+
types.Resource(
140+
uri="a2ui://recipe-form",
141+
name="Recipe Form",
142+
mimeType="application/json+a2ui",
143+
description="Static form allowing users to pick options.",
144+
)
145+
]
146+
147+
@app.read_resource()
148+
async def read_resource(uri: str) -> list[ReadResourceContents]:
149+
if uri == "a2ui://recipe-form":
150+
return [
151+
ReadResourceContents(
152+
content=json.dumps(recipe_form_json),
153+
mime_type="application/json+a2ui",
154+
)
155+
]
156+
raise ValueError(f"Unknown resource: {uri}")
157+
```
158+
159+
### 2. Dynamic UI via MCP Tools (`tools/call`)
160+
161+
For user interfaces that need to be generated dynamically based on the conversational context, user parameters, or real-time data, you should serve A2UI inside an MCP Tool's response.
162+
163+
- **Concept**: The client/Agent calls a tool with specific arguments (e.g., chosen ingredients, preferences), and the server returns a customized A2UI JSON wrapped inside an `EmbeddedResource` in the `CallToolResult`.
164+
- **Use Case**: Ideal for content that depends on live database queries, previous inputs, interactive step-by-step wizard state, or personalized recommendations (e.g., a customized recipe card).
165+
- **Benefit**: Maximizes flexibility, context-awareness, and supports highly dynamic flows.
166+
- **Best Practice (Fallback Text)**: Always include a `TextContent` alongside your `EmbeddedResource` in the `CallToolResult`. Clients that don't support A2UI will fall back to displaying this text to the user.
167+
168+
**Python Server Example:**
169+
170+
```python
171+
@app.call_tool()
172+
async def handle_call_tool(name: str, arguments: dict[str, Any]) -> types.CallToolResult:
173+
if name == "get_recipe_a2ui":
174+
# Resolve dynamic selections from client parameters
175+
style = arguments.get("cookingStyle", "Baked")
176+
protein = arguments.get("protein", "Salmon")
177+
178+
# Retrieve customized recipe database entry
179+
recipe_data = RECIPES.get((style, protein))
180+
181+
# Customize base A2UI schema dynamically
182+
custom_recipe_json = copy.deepcopy(recipe_a2ui_json)
183+
custom_recipe_json[1]["updateComponents"]["components"][0]["text"] = recipe_data["title"]
184+
185+
# Return customized recipe card as EmbeddedResource
186+
return types.CallToolResult(content=[
187+
types.EmbeddedResource(
188+
type="resource",
189+
resource=types.TextResourceContents(
190+
uri="a2ui://recipe-card",
191+
mimeType="application/json+a2ui",
192+
text=json.dumps(custom_recipe_json),
193+
)
194+
)
195+
])
196+
```
197+
71198
## Catalog Negotiation
72199
73200
Before a server can send A2UI to a client, they must establish which catalogs are available. Depending on your architecture, this can happen in one of two ways.
@@ -132,68 +259,6 @@ If your server must remain stateless, the client can pass A2UI capabilities in t
132259
}
133260
```
134261
135-
## Returning A2UI Content
136-
137-
A2UI content is returned as **Embedded Resources** inside a `CallToolResult`. Key rules:
138-
139-
- **URI**: Must use the `a2ui://` prefix with a descriptive name (e.g., `a2ui://training-plan-page`)
140-
- **MIME Type**: Must be `application/json+a2ui` — this tells the client to route the payload to an A2UI renderer
141-
142-
### Python Example
143-
144-
```python
145-
import json
146-
import mcp.types as types
147-
148-
@self.tool()
149-
def get_hello_world_ui():
150-
"""Returns a simple A2UI hello world interface."""
151-
a2ui_payload = [
152-
{
153-
"version": "v0.9",
154-
"createSurface": {
155-
"surfaceId": "default",
156-
"catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json"
157-
}
158-
},
159-
{
160-
"version": "v0.9",
161-
"updateComponents": {
162-
"surfaceId": "default",
163-
"components": [
164-
{
165-
"id": "root",
166-
"component": "Text",
167-
"text": "Hello World!"
168-
}
169-
]
170-
}
171-
}
172-
]
173-
174-
# Wrap A2UI as an Embedded Resource
175-
a2ui_resource = types.EmbeddedResource(
176-
type="resource",
177-
resource=types.TextResourceContents(
178-
uri="a2ui://hello-world",
179-
mimeType="application/json+a2ui",
180-
text=json.dumps(a2ui_payload),
181-
)
182-
)
183-
184-
# Include a text summary alongside the UI
185-
text_content = types.TextContent(
186-
type="text",
187-
text="Here is a hello world UI."
188-
)
189-
190-
return types.CallToolResult(content=[text_content, a2ui_resource])
191-
```
192-
193-
> TIP: Tip
194-
>
195-
> Always include a `TextContent` alongside your A2UI resource. Clients that don't support A2UI will fall back to showing the text.
196-
197262
## Handling User Actions
198263
199264
Interactive components like `Button` can trigger actions that are sent back to the server as MCP tool calls.

renderers/angular/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## Unreleased
22

3+
- (v0_9) Fix null de-referencing TypeError in `ComponentBinder` when `children` property is null or undefined. [#1472](https://github.com/google/A2UI/pull/1472)
34
- (v0_8) Fix Icon component to handle camelCase and TitleCase names by converting them to snake_case for `g-icon`.
45
- (v0_8) Fix Modal component styling and position fixed for overlay.
56
- (v0_9) Remove `placeholder` prop support from the `TextField` component, since it was not part of the v0_9 basic catalog schema. [#1372](https://github.com/google/A2UI/pull/1372)

0 commit comments

Comments
 (0)