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
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,11 @@ flowchart TD
The ecosystem is architected as a modular, highly cohesive monorepo utilizing
**Yarn v4 Workspaces**:

| Workspace | Package Name | Description & Core Responsibilities |
| :------------- | :----------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`shell/`** | `a2ui-composer-shell` | Standalone web application hosting chat panel, live JSON editors, real-time iframe preview wrapper, debugging suite, interactive mock rules manager, and IndexedDB storage engines. |
| **`bridge/`** | `a2ui-bridge` | ESBuild-bundled lightweight cross-frame JavaScript library embedded inside child rendering iframes. |
| **`samples/`** | `ng-basic-catalog`<br>`lit-basic-catalog`<br>`react-basic-catalog` | Plug-and-play developer renderer sandbox applications demonstrating zero-boilerplate integration across Lit, Angular, and React rendering stacks. |
| Workspace | Package Name | Description & Core Responsibilities |
| :------------- | :------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`shell/`** | `a2ui-composer-shell` | Standalone web application hosting chat panel, live JSON editors, real-time iframe preview wrapper, debugging suite, interactive mock rules manager, and IndexedDB storage engines. |
| **`bridge/`** | `a2ui-bridge` | ESBuild-bundled lightweight cross-frame JavaScript library embedded inside child rendering iframes. |
| **`samples/`** | `ng-basic-catalog`<br>`ng-basic-with-mcp-catalog`<br>`lit-basic-catalog`<br>`react-basic-catalog` | Plug-and-play developer renderer sandbox applications demonstrating zero-boilerplate integration across Lit, Angular, and React rendering stacks, plus an Angular renderer with MCP catalog integration. |

## Getting Started

Expand All @@ -118,14 +118,20 @@ yarn install
# ng-basic-catalog starts on localhost:3456
# lit-basic-catalog starts on localhost:3457
# react-basic-catalog starts on localhost:3458
# ng-basic-with-mcp-catalog starts on localhost:3459
yarn --cwd samples/ng-basic-catalog start
yarn --cwd samples/lit-basic-catalog start
yarn --cwd samples/react-basic-catalog start
yarn --cwd samples/ng-basic-with-mcp-catalog start

# Launch standalone interactive development shell on http://localhost:4200
yarn --cwd shell start
```

### Angular Basic with MCP Renderer (`ng-basic-with-mcp-catalog`)

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.

This comment seems out of place -- it takes the two paragraphs that used to follow and moves them into this new section, where they don't belong.


`samples/ng-basic-with-mcp-catalog` provides a standalone Angular renderer configured with the `basic_with_mcp` catalog (`https://a2ui.org/specification/v0_9/catalogs/basic_with_mcp/catalog.json`). Unlike the standard basic catalog renderers (`ng-basic-catalog`, `lit-basic-catalog`, and `react-basic-catalog`), this renderer exposes the Model Context Protocol (**MCP**) catalog functions (`callMcpTool`, `jmespath`, `split`, `regexCapture`, `regexReplace`, and `updateDataModel`). When selected in Settings alongside active HTTP MCP servers, the Gemini assistant automatically includes the discovered MCP tools in its system instructions so generated A2UI surfaces can invoke live MCP tools directly.

When the A2UI Composer starts, if this is your first time using it, you'll be
automatically routed to the Settings page, where you will need to enter the URL
of the renderer app you want to use (e.g., "http://localhost:3456).
Expand Down
8 changes: 8 additions & 0 deletions bridge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,17 @@
"prettier:check": "prettier --ignore-path ../.prettierignore --check .",
"lint": "eslint ."
},
"dependencies": {
"@a2ui/web_core": "0.10.5",
"@modelcontextprotocol/sdk": "1.30.0",
"jmespath": "0.16.0",
"re2js": "2.8.6",
"zod": "^4.1.12"

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.

We shouldn't use the ^ prefix. Along with the ~ prefix, it means we never really know what versions are being used.

},
"devDependencies": {
"@angular/core": "22.0.7",
"@lit/context": "1.1.6",
"@types/jmespath": "0.15.2",
"@types/react": "19.2.17",
"@types/react-dom": "19.2.3",
"@vitest/coverage-v8": "4.1.10",
Expand Down
16 changes: 16 additions & 0 deletions bridge/src/bridge-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export enum PreviewBridgeMessageType {
FORCE_UNBLOCK = 'FORCE_UNBLOCK',
GET_CATALOG = 'GET_CATALOG',
GET_COMPONENT_USAGES = 'GET_COMPONENT_USAGES',
MCP_REQUEST = 'MCP_REQUEST',
MCP_RESPONSE = 'MCP_RESPONSE',
RENDER_A2UI = 'RENDER_A2UI',
RENDER_ERROR = 'RENDER_ERROR',
RENDER_SUCCESS = 'RENDER_SUCCESS',
Expand Down Expand Up @@ -201,3 +203,17 @@ export declare interface A2uiComponentInstance {
id?: string;
[key: string]: unknown;
}

/** Payload for MCP_REQUEST message type. */
export declare interface McpRequestPayload {
requestId: string;
toolName: string;
args: Record<string, unknown>;
}

/** Payload for MCP_RESPONSE message type. */
export declare interface McpResponsePayload {
requestId: string;
result?: unknown;
error?: string;
}
74 changes: 74 additions & 0 deletions bridge/src/iframe-mcp-client.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* 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 {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
import {IframeMcpClient} from './iframe-mcp-client.js';

describe('IframeMcpClient', () => {
beforeEach(() => {
vi.useFakeTimers();
});

afterEach(() => {
vi.useRealTimers();
});

it('resolves callTool when matching MCP_RESPONSE is received', async () => {
const sentPayloads: Array<{
requestId: string;
toolName: string;
args: Record<string, unknown>;
}> = [];
const client = new IframeMcpClient(payload => {
sentPayloads.push(payload);
});

const promise = client.callTool({name: 'list_dir', arguments: {path: '/root'}});
expect(sentPayloads).toHaveLength(1);
expect(sentPayloads[0].toolName).toBe('list_dir');
expect(sentPayloads[0].args).toEqual({path: '/root'});

client.handleResponse({
requestId: sentPayloads[0].requestId,
result: {content: [{type: 'text', text: 'file1.txt'}]},
});

const result = await promise;
expect(result).toEqual({content: [{type: 'text', text: 'file1.txt'}]});
});

it('rejects callTool when MCP_RESPONSE contains error', async () => {
let reqId = '';
const client = new IframeMcpClient(payload => {
reqId = payload.requestId;
});

const promise = client.callTool({name: 'fail_tool'});
client.handleResponse({
requestId: reqId,
error: 'Server unreachable',
});

await expect(promise).rejects.toThrow('Server unreachable');
});

it('times out if no response is received within timeoutMs', async () => {
const client = new IframeMcpClient(() => {}, 1000);
const promise = client.callTool({name: 'slow_tool'});
vi.advanceTimersByTime(1001);
await expect(promise).rejects.toThrow(/timed out/);
});
});
73 changes: 73 additions & 0 deletions bridge/src/iframe-mcp-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* 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 {McpRequestPayload, McpResponsePayload} from './bridge-message.js';

interface PendingRequest {
resolve: (value: unknown) => void;
reject: (reason?: unknown) => void;
timer: ReturnType<typeof setTimeout>;
}

/**
* Proxies MCP `callTool` requests from inside the preview iframe to the parent shell
* via postMessage (`MCP_REQUEST` / `MCP_RESPONSE`).
*/
export class IframeMcpClient {
private pendingRequests = new Map<string, PendingRequest>();

constructor(
private readonly sendMcpRequest: (payload: McpRequestPayload) => void,
private readonly timeoutMs = 30000,
) {}

async callTool(params: {name: string; arguments?: Record<string, unknown>}): Promise<unknown> {
const requestId =
typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
? crypto.randomUUID()
: `mcp-${Date.now()}-${Math.random().toString(36).slice(2)}`;

return new Promise<unknown>((resolve, reject) => {
const timer = setTimeout(() => {
this.pendingRequests.delete(requestId);
reject(new Error(`MCP tool call "${params.name}" timed out after ${this.timeoutMs}ms`));
}, this.timeoutMs);

this.pendingRequests.set(requestId, {resolve, reject, timer});

this.sendMcpRequest({
requestId,
toolName: params.name,
args: params.arguments ?? {},
});
});
}

handleResponse(payload: McpResponsePayload): void {
const pending = this.pendingRequests.get(payload.requestId);
if (!pending) {
return;
}
clearTimeout(pending.timer);
this.pendingRequests.delete(payload.requestId);

if (payload.error) {
pending.reject(new Error(payload.error));
} else {
pending.resolve(payload.result);
}
}
}
3 changes: 3 additions & 0 deletions bridge/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
export * from './bridge-message.js';
export * from './domain-origin-verification.js';
export * from './instrumentation-overrides.js';
// TODO: Replace './mcp/index.js' with '@a2ui/mcp-catalog/v0_9' once published to NPM.
export * from './mcp/index.js';
export * from './iframe-mcp-client.js';
export * from './preview-bridge.js';
export * from './render-config.js';
export * from './surface-resize-observer.js';
95 changes: 95 additions & 0 deletions bridge/src/mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Vendored A2UI MCP Catalog

This directory contains vendored TypeScript source files from the upstream
`@a2ui/mcp-catalog` package (version `v0_9`).

## Summary

The upstream package `@a2ui/mcp-catalog` is not yet published to the NPM registry
and remains private within the A2UI repository. Composer requires the MCP catalog
functions to support Model Context Protocol (MCP) tool execution and dynamic data
transformations in preview surfaces.

To unblock Composer development without waiting for the upstream release, we
vendor the catalog source files into this isolated directory. Once the upstream
package is published to NPM, remove this directory and reference the official
NPM package.

## Scope and Non-Scope

- **Scope**: Catalog function implementations, schemas, dynamic expression
resolvers, and unit tests adhering to the A2UI MCP catalog specification.
- **Non-Scope**: Composer bridge infrastructure and iframe communication. The
`IframeMcpClient` class resides in `bridge/src/iframe-mcp-client.ts`, outside
this directory.

## File Inventory and Upstream Mapping

The following table maps each vendored file in this directory to its upstream
source in `a2ui/catalogs/mcp/v0_9/`:

| Local Path | Upstream Source | Description |
| :----------------------------- | :--------------------------------- | :--------------------------------------------------------------------------- |
| `index.ts` | `src/index.ts` | Catalog function factory (`createMcpCatalogFunctions`) and catalog constants |
| `dynamic-values.ts` | `src/dynamic-values.ts` | Recursive resolution of data bindings and dynamic function calls |
| `functions/callMcpTool.ts` | `src/functions/callMcpTool.ts` | MCP tool execution function and UI resource extractor |
| `functions/common.ts` | `src/functions/common.ts` | Schema helpers and Promise settling utilities |
| `functions/jmespath.ts` | `src/functions/jmespath.ts` | JMESPath query evaluation function |
| `functions/regexCapture.ts` | `src/functions/regexCapture.ts` | RE2 regular expression capture group extractor |
| `functions/regexReplace.ts` | `src/functions/regexReplace.ts` | RE2 regular expression replacement function |
| `functions/split.ts` | `src/functions/split.ts` | String delimiter split function |
| `functions/updateDataModel.ts` | `src/functions/updateDataModel.ts` | Surface data model mutation function |
| `mcp.spec.ts` | `src/**/*.test.ts` | Vitest test suite adapted from upstream unit tests |

## Temporary Dependencies

The `bridge/package.json` file includes several dependencies solely to support
these vendored files:

- `@modelcontextprotocol/sdk`: MCP client type definitions and result schemas.
- `jmespath`: Query engine for the `jmespath` catalog function.
- `re2js`: Linear-time regular expression engine for `regexCapture` and `regexReplace`.
- `zod`: Runtime schema validation for function signatures.
- `@types/jmespath`: TypeScript definitions for JMESPath.

## Migration Steps

Follow these steps to replace this vendored directory once `@a2ui/mcp-catalog`
is published to NPM:

1. Add `@a2ui/mcp-catalog` to `bridge/package.json`:

```bash
yarn workspace a2ui-bridge add @a2ui/mcp-catalog
```

2. Remove temporary dependencies from `bridge/package.json`:

Remove `jmespath`, `re2js`, `zod`, `@types/jmespath`, and
`@modelcontextprotocol/sdk` (unless required elsewhere in `a2ui-bridge`).

3. Update `bridge/src/index.ts` to re-export from the NPM package:

```typescript
export * from '@a2ui/mcp-catalog/v0_9';
export * from './iframe-mcp-client.js';
```

4. Delete this directory:

```bash
rm -rf bridge/src/mcp
```

5. Update `samples/ng-basic-with-mcp-catalog`:

- Add `@a2ui/mcp-catalog` to `samples/ng-basic-with-mcp-catalog/package.json`.
- In `samples/ng-basic-with-mcp-catalog/src/app/basic-with-mcp.catalog.ts`,
replace the import from `a2ui-bridge` with `@a2ui/mcp-catalog/v0_9`.

6. Run the test suite and linter to verify the migration:

```bash
yarn test
yarn lint
```
Loading
Loading