diff --git a/README.md b/README.md
index 1eb6e110e..c3311b45f 100644
--- a/README.md
+++ b/README.md
@@ -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` `lit-basic-catalog` `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` `ng-basic-with-mcp-catalog` `lit-basic-catalog` `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
@@ -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`)
+
+`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).
diff --git a/bridge/package.json b/bridge/package.json
index edc5589b3..ec3655c99 100644
--- a/bridge/package.json
+++ b/bridge/package.json
@@ -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"
+ },
"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",
diff --git a/bridge/src/bridge-message.ts b/bridge/src/bridge-message.ts
index 7ed544bdc..b91364a36 100644
--- a/bridge/src/bridge-message.ts
+++ b/bridge/src/bridge-message.ts
@@ -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',
@@ -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;
+}
+
+/** Payload for MCP_RESPONSE message type. */
+export declare interface McpResponsePayload {
+ requestId: string;
+ result?: unknown;
+ error?: string;
+}
diff --git a/bridge/src/iframe-mcp-client.spec.ts b/bridge/src/iframe-mcp-client.spec.ts
new file mode 100644
index 000000000..939f741e5
--- /dev/null
+++ b/bridge/src/iframe-mcp-client.spec.ts
@@ -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;
+ }> = [];
+ 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/);
+ });
+});
diff --git a/bridge/src/iframe-mcp-client.ts b/bridge/src/iframe-mcp-client.ts
new file mode 100644
index 000000000..4266d73ab
--- /dev/null
+++ b/bridge/src/iframe-mcp-client.ts
@@ -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;
+}
+
+/**
+ * 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();
+
+ constructor(
+ private readonly sendMcpRequest: (payload: McpRequestPayload) => void,
+ private readonly timeoutMs = 30000,
+ ) {}
+
+ async callTool(params: {name: string; arguments?: Record}): Promise {
+ const requestId =
+ typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
+ ? crypto.randomUUID()
+ : `mcp-${Date.now()}-${Math.random().toString(36).slice(2)}`;
+
+ return new Promise((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);
+ }
+ }
+}
diff --git a/bridge/src/index.ts b/bridge/src/index.ts
index afa864c72..ead5b52f2 100644
--- a/bridge/src/index.ts
+++ b/bridge/src/index.ts
@@ -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';
diff --git a/bridge/src/mcp/README.md b/bridge/src/mcp/README.md
new file mode 100644
index 000000000..a3d35eec7
--- /dev/null
+++ b/bridge/src/mcp/README.md
@@ -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
+ ```
diff --git a/bridge/src/mcp/dynamic-values.ts b/bridge/src/mcp/dynamic-values.ts
new file mode 100644
index 000000000..8ca98acef
--- /dev/null
+++ b/bridge/src/mcp/dynamic-values.ts
@@ -0,0 +1,143 @@
+/*
+ * 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
+ *
+ * https://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.
+ */
+
+/**
+ * TODO: Replace this file with imports from `@a2ui/mcp-catalog/v0_9` once published to NPM.
+ *
+ * Type guards and recursive resolution utilities for A2UI dynamic values.
+ *
+ * While `DataContext.resolveDynamicValue` resolves a single top-level `DynamicValue`,
+ * these helpers recursively traverse nested objects and arrays to resolve embedded
+ * data bindings and function calls.
+ */
+
+import type {DataBinding, DataContext, FunctionCall} from '@a2ui/web_core/v0_9';
+
+/** Union of dynamic expression types (`DataBinding` or `FunctionCall`) that require runtime evaluation. */
+export type DynamicExpression = DataBinding | FunctionCall;
+
+/** Maximum recursion depth when resolving nested dynamic values to prevent infinite loops. */
+const MAX_RESOLUTION_DEPTH = 100;
+
+/** Checks whether a value is a non-null, non-array object. */
+function isRecord(value: unknown): value is Record {
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
+}
+
+/**
+ * Checks whether `value` is a `DataBinding` (`{path: string}`).
+ *
+ * Objects with additional properties alongside `path` are treated as literal objects.
+ */
+export function isDataBinding(value: unknown): value is DataBinding {
+ return isRecord(value) && typeof value['path'] === 'string' && Object.keys(value).length === 1;
+}
+
+/**
+ * Checks whether `value` is a `FunctionCall` (`{call: string, args?, returnType?}`).
+ */
+export function isFunctionCall(value: unknown): value is FunctionCall {
+ if (!isRecord(value) || typeof value['call'] !== 'string') {
+ return false;
+ }
+ const args = value['args'];
+ const returnType = value['returnType'];
+ return (
+ (args === undefined || isRecord(args)) &&
+ (returnType === undefined || typeof returnType === 'string')
+ );
+}
+
+/** Checks whether `value` is a `DataBinding` or a `FunctionCall`. */
+export function isDynamicExpression(value: unknown): value is DynamicExpression {
+ return isDataBinding(value) || isFunctionCall(value);
+}
+
+/**
+ * Recursively resolves all dynamic expressions (`DataBinding` and `FunctionCall`)
+ * nested within `value`.
+ *
+ * Traverses arrays and plain objects while preserving their structure, and
+ * repeatedly resolves chained expressions until a concrete value is reached or
+ * `MAX_RESOLUTION_DEPTH` is hit. Pending Promises from async functions are
+ * passed through as-is.
+ *
+ * @param value The value or container to resolve.
+ * @param context The data context used for evaluation.
+ * @param depth Internal recursion depth counter.
+ */
+export function resolveDynamicValueDeep(
+ value: unknown,
+ context: DataContext,
+ depth = 0,
+): T {
+ if (depth >= MAX_RESOLUTION_DEPTH) {
+ return value as T;
+ }
+ if (!isRecord(value) && !Array.isArray(value)) {
+ return value as T;
+ }
+ if (typeof (value as {then?: unknown}).then === 'function') {
+ return value as T;
+ }
+ if (Array.isArray(value)) {
+ return value.map(item => resolveDynamicValueDeep(item, context, depth + 1)) as unknown as T;
+ }
+ if (isDataBinding(value)) {
+ const resolved = context.resolveDynamicValue(value);
+ // Stop if the binding could not be resolved to avoid an infinite loop.
+ if (resolved === value) {
+ return resolved as unknown as T;
+ }
+ return resolveDynamicValueDeep(resolved, context, depth + 1);
+ }
+ if (isFunctionCall(value)) {
+ // Supply default values for optional `args` and `returnType` fields before resolving.
+ const dynamicCall =
+ value.args !== undefined && value.returnType !== undefined
+ ? value
+ : {
+ call: value.call,
+ args: value.args ?? {},
+ returnType: value.returnType ?? 'any',
+ };
+ const resolved = context.resolveDynamicValue(dynamicCall);
+ if (resolved === value || resolved === dynamicCall) {
+ return resolved as unknown as T;
+ }
+ return resolveDynamicValueDeep(resolved, context, depth + 1);
+ }
+ return resolveDynamicRecord(value, context, depth) as T;
+}
+
+/**
+ * Resolves each property value in a key-value record (such as function arguments).
+ *
+ * Resolves entries individually rather than evaluating the record itself as a
+ * dynamic expression, so records containing `path` or `call` keys are preserved
+ * as plain objects.
+ */
+export function resolveDynamicRecord(
+ record: Record,
+ context: DataContext,
+ depth = 0,
+): Record {
+ const resolved: Record = {};
+ for (const [key, value] of Object.entries(record)) {
+ resolved[key] = resolveDynamicValueDeep(value, context, depth + 1);
+ }
+ return resolved;
+}
diff --git a/bridge/src/mcp/functions/callMcpTool.ts b/bridge/src/mcp/functions/callMcpTool.ts
new file mode 100644
index 000000000..42ff8a1a2
--- /dev/null
+++ b/bridge/src/mcp/functions/callMcpTool.ts
@@ -0,0 +1,311 @@
+/*
+ * 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
+ *
+ * https://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.
+ */
+
+/**
+ * TODO: Replace this file with imports from `@a2ui/mcp-catalog/v0_9` once published to NPM.
+ */
+
+import {
+ A2uiExpressionError,
+ DynamicStringSchema,
+ DynamicValueSchema,
+ createFunctionImplementation,
+ type A2uiMessage,
+ type CreateSurfaceMessage,
+ type FunctionImplementation,
+ type SurfaceModel,
+} from '@a2ui/web_core/v0_9';
+import type {Client} from '@modelcontextprotocol/sdk/client/index.js';
+import {CallToolResultSchema} from '@modelcontextprotocol/sdk/types.js';
+import type {CallToolResult, ReadResourceResult} from '@modelcontextprotocol/sdk/types.js';
+import {z} from 'zod/v3';
+
+import {resolveDynamicRecord} from '../dynamic-values.js';
+import type {FunctionApiDefinition} from './common.js';
+
+/** MIME type identifying an A2UI payload in an MCP resource. */
+export const A2UI_MIME_TYPE = 'application/a2ui+json';
+
+/** Maximum number of decoded UI resources to cache per implementation. */
+const MAX_CACHED_RESOURCES = 100;
+
+/** Subset of the MCP `Client` or `IframeMcpClient` interface used by this catalog. */
+export interface McpToolClient {
+ callTool?: (params: {name: string; arguments?: Record}) => Promise;
+ request?: Client['request'];
+ readResource?: Client['readResource'];
+ listTools?: Client['listTools'];
+}
+
+/** Processor interface capable of processing A2UI messages and optionally looking up surfaces. */
+export interface McpMessageProcessor {
+ processMessages(messages: A2uiMessage[]): void;
+ model?: {
+ getSurface?(surfaceId: string): SurfaceModel | undefined;
+ };
+}
+
+/**
+ * Resolves the connected MCP client for a given tool name.
+ *
+ * May return `null` or `undefined` if no client is currently available,
+ * which causes the tool call to fail with an error.
+ */
+export type McpClientResolver = (
+ toolName: string,
+) => McpToolClient | undefined | null | Promise;
+
+/**
+ * Function API definition for `callMcpTool`.
+ *
+ * Payloads reference tools by name; the host application is responsible for
+ * routing each tool name to the appropriate MCP server client.
+ */
+export const CallMcpToolApi: FunctionApiDefinition = {
+ name: 'callMcpTool',
+ returnType: 'any',
+ schema: z.object({
+ name: (DynamicStringSchema as unknown as z.ZodTypeAny).describe(
+ 'The name of the MCP tool to execute.',
+ ),
+ arguments: z
+ .record(DynamicValueSchema as unknown as z.ZodTypeAny)
+ .optional()
+ .default({})
+ .describe('The arguments to pass to the MCP tool.'),
+ }) as unknown as FunctionApiDefinition['schema'],
+};
+
+/**
+ * Creates the `callMcpTool` function implementation.
+ *
+ * Invokes the named MCP tool, processes any referenced or inline `application/a2ui+json`
+ * resources through `processor`, and returns the raw `CallToolResult`.
+ *
+ * @param getMcpClientForTool Callback that resolves the MCP client for a tool name.
+ * @param processor Message processor that receives A2UI messages decoded from tool results.
+ */
+export function createCallMcpToolImplementation(
+ getMcpClientForTool: McpClientResolver,
+ processor: McpMessageProcessor,
+): FunctionImplementation {
+ const declaredUiResourceUris = new WeakMap>>();
+ const cachedA2uiResources = new Map();
+
+ async function readA2uiResource(client: McpToolClient, uri: string): Promise {
+ const cached = cachedA2uiResources.get(uri);
+ if (cached !== undefined) {
+ cachedA2uiResources.delete(uri);
+ cachedA2uiResources.set(uri, cached);
+ return cached;
+ }
+
+ if (!client.readResource) {
+ return [];
+ }
+ const resource = await client.readResource({uri});
+ const messages = parseA2uiMessages(resource, uri);
+ if (cachedA2uiResources.size >= MAX_CACHED_RESOURCES) {
+ const oldestKey = cachedA2uiResources.keys().next().value;
+ if (oldestKey !== undefined) {
+ cachedA2uiResources.delete(oldestKey);
+ }
+ }
+ cachedA2uiResources.set(uri, messages);
+ return messages;
+ }
+
+ function getDeclaredUiResourceUris(client: McpToolClient): Promise