Repository navigation
feat: add A2UI MCP catalog renderer and live HTTP MCP server support #219
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
2c498d9
feat: implement A2UI MCP catalog and live MCP server support
wrenj 7e29a54
feat: switch chat model to gemini-3.7-flash with medium thinking
wrenj 42455af
e2e flow works
wrenj f1bdd73
show mcp_response
wrenj 73309ef
a
wrenj 0a2e128
separate renderer for basic with mcp
wrenj 89a65b7
fix prompt
wrenj bdb37d9
styling
wrenj 176c7c1
a
wrenj eabfbfb
a
wrenj 37e54c3
aa
wrenj 0d48d36
aa
wrenj da1747e
aaa
wrenj fe036e8
chore: remove unused renderer changes and point MCP sample to local port
wrenj 5c4adc4
fix: harden MCP type guards, race conditions, and payload validation
wrenj cab80bc
docs: document vendored MCP catalog and isolate for future NPM import
wrenj File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We shouldn't use the |
||
| }, | ||
| "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", | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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/); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| ``` |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.