Skip to content
Open
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
6 changes: 6 additions & 0 deletions catalogs/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ For `updateDataModel`, keys starting with `/` are absolute paths, while relative

The v1 catalog, [v1/catalog.json](v1/catalog.json), targets A2UI protocol v1.0. Its catalog ID is `https://a2ui.org/specification/v1_0/catalogs/mcp/catalog.json`. It declares `callMcpTool` and `updateDataModel`, with argument and return types taken from the v1.0 `common_types.json`, and the `McpApp` component.

### Tool data payload

In the v1 catalog, `callMcpTool` returns a payload with `toolName`, `toolInput`, `toolResult` (the raw `CallToolResult`), `structuredContent` (when the server sends one), `text` (the `text` blocks joined with `\n`) and `json` (the parsed value of `text` when it is a JSON object or array; when instead several text blocks are each a JSON object or array, the array of their parsed values; absent otherwise). When the result has an MCP App resource, the payload also carries its `resourceUri`, `html`, `csp` and `permissions`.

The optional `targetDataPath` argument writes the payload of each successful call to that path, replacing the object there. A call whose response arrives after a newer call to the same path has started does not write, so the latest call wins. A result flagged `isError` fails the call and writes nothing.

### Data functions

| Function | Arguments | Returns |
Expand Down
4 changes: 2 additions & 2 deletions catalogs/mcp/v1/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
"functions": {
"callMcpTool": {
"type": "object",
"description": "Invokes a tool on a connected Model Context Protocol (MCP) server and returns the result.",
"description": "Invokes a tool on a connected Model Context Protocol (MCP) server and returns a payload with `toolName`, `toolInput`, `toolResult` (the raw result), `structuredContent` (when the server sends one), `text` (the text blocks joined with newlines) and `json` (the parsed value of `text` when it is a JSON object or array, else the array of parsed values when several text blocks are each one, absent otherwise), plus `resourceUri`, `html`, `csp` and `permissions` when the result has an MCP App resource. A result flagged `isError` fails the call and writes nothing.",
"returnType": "any",
"properties": {
"@call": {
Expand All @@ -138,7 +138,7 @@
},
"targetDataPath": {
"$ref": "common_types.json#/$defs/DynamicString",
"description": "Optional JSON Pointer path on the surface data model where a resolved MCP App resource payload (`resourceUri`, `html`, `csp`, `permissions`, `toolInput`, `toolResult`) is written."
"description": "Optional JSON Pointer path on the surface data model where the payload of each successful call is written, replacing the object at that path. A call whose response arrives after a newer call to the same path has started does not write, so the latest call wins."
}
},
"required": ["name"],
Expand Down
6 changes: 6 additions & 0 deletions conformance/core/catalog.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,12 @@
protocolVersion: "v0.9"
catalogPath: "catalogs/mcp/catalog.json"

- name: test_v10_mcp_catalog_round_trip
description: Tests that the published v1 MCP catalog round-trips with its McpApp component, its functions, their common_types.json references and its $defs unions intact.
action: round_trip
protocolVersion: "1.0"
catalogPath: "catalogs/mcp/v1/catalog.json"

- name: test_v10_custom_catalog_round_trip_function_metadata
description: Tests that function descriptions, return types, allowedCallers and requiresUserActivation round-trip in the v1.0 function shape. The basic catalogs declare no allowedCallers.
action: round_trip
Expand Down
1 change: 1 addition & 0 deletions typescript/catalogs/mcp/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
- Emit `'1.0'` as `version` on synthesized `createSurface` and `updateComponents` messages in `callMcpTool` when creating a standalone MCP App surface, matching the unified v1.0 protocol version wire format. [#3097](https://github.com/a2ui-project/a2ui/issues/3097)
- The published package contains only the runtime build output; test files and TypeScript build info are no longer included. [#3348](https://github.com/a2ui-project/a2ui/pull/3348)
- **BREAKING CHANGE**: Hosts set up the catalog once with `configureMcpCatalog`; `createCallMcpToolImplementation`, `CallMcpToolApi` and `A2UI_MIME_TYPE` are removed and `MCP_CATALOG_ID` now identifies the v1.0 catalog (`catalogs/mcp/v1/catalog.json`, bundled as `@a2ui/catalog-mcp/catalog.json`). `callMcpTool` now hands `ui://` MCP App resources to the host. [#3186](https://github.com/a2ui-project/a2ui/pull/3186)
- `callMcpTool` returns a payload with `toolName`, `toolInput`, `toolResult`, `structuredContent`, `text` and `json`, and writes it to `targetDataPath` when set, where the latest call to a path wins. [#3728](https://github.com/a2ui-project/a2ui/pull/3728)
- The `updateDataModel` data function writes key-value pairs to the surface data model; its `UpdateDataModelApi` and `UpdateDataModelImplementation` are exported for hosts that register functions one by one. [#3573](https://github.com/a2ui-project/a2ui/pull/3573)
- Add `mcpCatalog`, the v1.0 MCP catalog with the `McpApp` universal component (`A2uiMcpApp`) and `callMcpTool`, ready to hand to a `MessageProcessor`. `McpApp` renders an MCP App in the shared sandbox, binds host data through `data.paths` and dispatches the tools it is allowed to call as A2UI actions. [#2799](https://github.com/a2ui-project/a2ui/pull/2799)

Expand Down
44 changes: 24 additions & 20 deletions typescript/catalogs/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ A2UI separates UI layout from backend logic through catalogs. This catalog provi
- The host supplies one hook, `getMcpClientForTool(toolName)`. The catalog uses the returned client for tool execution, UI resource reads, and tool discovery.
- Tool results are processed automatically. The catalog fetches and caches UI resources referenced by results, then applies any A2UI messages embedded in the result content.
- Tool arguments support data bindings, allowing form state to pass directly into tool calls.
- `updateDataModel` writes path-value pairs to the surface data model, so a payload can update several paths at once from a literal object, a binding or a function call.
- `callMcpTool` returns the text, parsed JSON and structured content of each result, and can write them to `targetDataPath`. `updateDataModel` writes path-value pairs to the surface data model, so a payload can update several paths at once from a literal object, a binding or a function call.
- The `McpApp` component embeds MCP Apps in a sandboxed double-iframe and connects them to the host through `AppBridge`.
- The catalog works with any A2UI web renderer built on `MessageProcessor`.

Expand All @@ -25,11 +25,22 @@ The canonical schema lives at [`catalogs/mcp/v1/catalog.json`](../../../catalogs
| :--------------- | :-------------- | :---------------- | :--------------------------------------------------------------------------------------------------------- |
| `name` | `DynamicString` | Yes | The MCP tool to execute. |
| `arguments` | `object` | No (default `{}`) | Arguments passed to the tool: an object of dynamic values, or a function call that returns one. |
| `targetDataPath` | `DynamicString` | No | Optional JSON Pointer path on the surface data model where a resolved MCP App resource payload is written. |
| `targetDataPath` | `DynamicString` | No | Optional JSON Pointer path on the surface data model where the payload of each successful call is written. |

Tools are addressed by name only. A2UI payloads never name a server, because multi-server routing is resolved by the host inside `getMcpClientForTool`.

The function returns the raw MCP `CallToolResult`. It throws an `A2uiExpressionError` if the client cannot be resolved, the call returns no result, or the result has `isError: true`.
The function returns a payload with these fields:

| Field | Value |
| :------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `toolName` | The name of the tool. |
| `toolInput` | The resolved arguments. |
| `toolResult` | The raw MCP `CallToolResult`. |
| `structuredContent` | `toolResult.structuredContent`, unchanged, when the server sends one. |
| `text` | The `text` content blocks, joined with `\n`. |
| `json` | The parsed value of `text` when it is a JSON object or array. When instead two or more text blocks are each a JSON object or array, the array of their parsed values. Absent otherwise. |

When the result has an MCP App resource, the payload also carries its `resourceUri`, `html`, `csp` and `permissions`. With `targetDataPath`, each call replaces the object at that path with its payload. A call whose response arrives after a newer call to the same path has started does not write, so out-of-order responses, such as autocomplete suggestions, never overwrite newer ones; it still returns its payload. The function throws an `A2uiExpressionError`, and writes nothing, if the client cannot be resolved, the call returns no result, or the result has `isError: true`.

`updateDataModel` writes path-value pairs to the surface data model:

Expand Down Expand Up @@ -81,7 +92,7 @@ Push every catalog before the first message arrives, since `getClientCapabilitie

A surface resolves components and function calls through its default catalog, the `catalogId` of its `createSurface` message, unless the component or the `@call` object names another registered catalog with its own `catalogId`. A payload that mixes basic components with `McpApp` and the MCP functions therefore needs no composed catalog: it creates the surface with the basic catalog id and sets `"catalogId": "https://a2ui.org/specification/v1_0/catalogs/mcp/catalog.json"` on the `McpApp` component and on each MCP function call, as the [catalog examples](../../../catalogs/mcp/v1/examples) do.

Function calls in expressions (`${...}`, `@call`) are the exception: they always resolve through the surface's default catalog. A payload that calls `callMcpTool` from expressions next to basic components creates its surface with a catalog that carries both; build one under the id the agent uses (`mcpCatalog.id` is the canonical one):
Function calls inside `${...}` string expressions are the exception: they resolve through the surface's default catalog. A payload that calls `callMcpTool` that way next to basic components creates its surface with a catalog that carries both; build one under the id the agent uses (`mcpCatalog.id` is the canonical one):

```typescript
import {basicCatalog, Catalog, MessageProcessor} from '@a2ui/web_core/v1_0';
Expand Down Expand Up @@ -130,7 +141,7 @@ On each invocation the catalog:
4. Fetches each UI resource through `resources/read`, once per URI, and decodes every content block whose `mimeType` is `application/a2ui+json`. A resource carrying several such blocks contributes all of them, and one carrying none contributes nothing.
5. Processes each UI resource in the order its URI appeared, skipping any that would recreate a live surface.
6. Processes the A2UI messages inlined in `result.content`, in content order. Only an embedded resource block declaring `application/a2ui+json` counts: a text block is prose for the model, even when it holds a message.
7. Returns the `CallToolResult` unchanged to the caller.
7. Builds the payload, writes it to `targetDataPath` unless a newer call to that path has started, and returns it.

### Invoke a tool during bootstrap

Expand Down Expand Up @@ -160,29 +171,22 @@ await CallMcpToolImplementation.execute(

### Trigger tools from A2UI payloads

A surface created under a catalog that includes `callMcpTool` can invoke server tools:
A v1.0 payload calls `callMcpTool` from any surface whose host registered `mcpCatalog`, naming the catalog on the call. Arguments may be data bindings, which resolve against the calling surface, and `targetDataPath` writes the returned payload into the data model so components can bind to its fields (such as `/weather/json/temperature` or `/weather/text`):

```json
{
"createSurface": {
"surfaceId": "weather-widget",
"catalogId": "https://a2ui.org/specification/v0_9/catalogs/mcp/mcp_catalog.json"
}
}
```

Arguments may be data bindings, which resolve against the calling surface:

```json
{
"call": "callMcpTool",
"@call": "callMcpTool",
"catalogId": "https://a2ui.org/specification/v1_0/catalogs/mcp/catalog.json",
"args": {
"name": "get_weather",
"arguments": {"city": {"path": "/form/city"}}
"arguments": {"city": {"@path": "/form/city"}},
"targetDataPath": "/weather"
}
}
```

A surface created with the MCP catalog id as its default needs no `catalogId` on the call.

## The `McpApp` component

`McpApp` renders an [MCP App](https://github.com/modelcontextprotocol/ext-apps) in a surface. The app is an HTML document the agent sends in the component's `htmlContent`; it runs in a sandboxed inner frame behind a proxy page served from the host's origin (see [Sandbox asset](#sandbox-asset)) and talks to the host with the MCP Apps JSON-RPC protocol over `postMessage`, using `AppBridge` from `@modelcontextprotocol/ext-apps` on the host side. An app written with the MCP Apps App SDK works as it is; the [catalog examples](../../../catalogs/mcp/v1/examples) show the raw protocol.
Expand Down Expand Up @@ -332,7 +336,7 @@ The component owns its bridge: `McpAppBridge` (`src/components/mcp_app_bridge.ts
| `src/index.ts` | Package entry: `mcpCatalog`, `A2uiMcpApp`, `configureMcpCatalog` and the `*Api` and `*Implementation` of every function |
| `src/catalog.ts` | `MCP_CATALOG_ID` and `mcpCatalog`: the `McpApp` component, `callMcpTool` and `updateDataModel` |
| `src/mcp_catalog_config.ts` | `configureMcpCatalog`: the host's MCP client resolver, message processor and sandbox location |
| `src/functions/callMcpTool.ts` | MCP tool execution, UI resource discovery, caching, and message decoding |
| `src/functions/callMcpTool.ts` | MCP tool execution, the tool data payload, UI resource discovery, caching, and message decoding |
| `src/functions/update_data_model.ts` | `updateDataModel` |
| `src/dynamic-values.ts` | Resolution of dynamic values nested in literal containers |
| `src/components/mcp_app.ts` | The `McpApp` universal component and its API |
Expand Down
5 changes: 4 additions & 1 deletion typescript/catalogs/mcp/src/components/mcp_app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from '@a2ui/web_core/v1_0';
import type {CallToolResult} from '@modelcontextprotocol/sdk/types.js';
import {z} from 'zod';
import type {McpToolDataPayload} from '../functions/callMcpTool.js';
import {getMcpCatalogConfig} from '../mcp_catalog_config.js';
import {ComponentContextFrameHost} from '../shared/sandbox/component_context_frame_host.js';
import type {
Expand Down Expand Up @@ -201,7 +202,9 @@ class McpAppElement extends SandboxedFrameElement<typeof McpAppApi> {
// Executed directly rather than through the catalog invoker: the invoker validates the
// arguments as A2UI payload values, which excludes the plain objects an app may send, and
// the bridge has already checked them for prototype pollution, depth and size.
const res = await callMcpTool.execute({name, arguments: args}, this.context.dataContext);
const payload = await callMcpTool.execute({name, arguments: args}, this.context.dataContext);
// `callMcpTool` returns its tool data payload; the app receives the raw result inside it.
const res = (payload as Partial<McpToolDataPayload> | undefined)?.toolResult ?? payload;
if (res && typeof res === 'object' && Array.isArray((res as CallToolResult).content)) {
return res as CallToolResult;
}
Expand Down
Loading
Loading