Skip to content

Call MCP Tools from A2UI

The @a2ui/catalog-mcp package connects A2UI v1.0 or later web surfaces directly to Model Context Protocol (MCP) servers so buttons, inputs, and surface actions can call MCP tools and write their JSON or text results straight into the surface Data Model without waiting for another round-trip to the agent.


Quick setup

Setting up @a2ui/catalog-mcp takes three steps.

Install the packages

Install @a2ui/catalog-mcp alongside @a2ui/web_core, lit, and the MCP TypeScript SDK:

Terminal window
npm install @a2ui/catalog-mcp @a2ui/web_core lit @modelcontextprotocol/sdk

@a2ui/web_core and lit are peer dependencies so your renderer and catalog share one copy of A2UI’s core classes, and @modelcontextprotocol/sdk provides the MCP Client that connects to your server.

Register the catalog in your app

Connect an MCP Client to your server, pass mcpCatalog alongside basicCatalog (or your own component catalog) to MessageProcessor, and call configureMcpCatalog:

import {configureMcpCatalog, mcpCatalog} from '@a2ui/catalog-mcp';
import {basicCatalog, MessageProcessor} from '@a2ui/web_core/v1_0';
import {Client} from '@modelcontextprotocol/sdk/client/index.js';
import {StreamableHTTPClientTransport} from '@modelcontextprotocol/sdk/client/streamableHttp.js';
// 1. Connect to your MCP server.
const mcpClient = new Client({name: 'my-web-app', version: '1.0.0'});
await mcpClient.connect(new StreamableHTTPClientTransport(new URL('http://localhost:8000/mcp')));
// 2. Register mcpCatalog alongside basicCatalog.
const processor = new MessageProcessor([basicCatalog, mcpCatalog], action => {
console.log('Action dispatched:', action);
});
// 3. Tell the catalog how to reach your MCP client and message processor.
configureMcpCatalog({
getMcpClientForTool: () => mcpClient,
processor,
});

getMcpClientForTool receives the name of the tool being called and returns the MCP client that executes it. When your app connects to one MCP server, return that client for every call; for multiple servers, see Advanced configuration and troubleshooting.

Share the catalog schema with your agent

Include the MCP catalog schema in your agent setup so it knows how to emit MCP tool calls and McpApp components:

  1. Load @a2ui/catalog-mcp/catalog.json (or catalogs/mcp/v1/catalog.json on non-Node backends) alongside your component catalog when building your agent’s prompt and schema validator.
  2. Advertise the MCP catalog ID (https://a2ui.org/specification/v1_0/catalogs/mcp/catalog.json, exported as mcpCatalog.id and MCP_CATALOG_ID) in supportedCatalogIds. Calling processor.getRendererCapabilities({versions: ['1.0']}) includes every registered catalog automatically.

You are ready to call MCP tools from your UI. If you also embed McpApp HTML widgets, complete the extra sandbox file step in Embed MCP Apps in A2UI Surfaces.


Calling MCP tools from your UI

An A2UI component can invoke an MCP tool when a user clicks a button or types into a field. Because the browser calls your MCP server through callMcpTool, the surface updates right away without another agent turn.

Store tool output at a data path

The easiest way to call a tool is to pass targetDataPath. When the tool finishes, callMcpTool writes the response object to that path in the surface data model so your components can bind to it directly.

For example, this button action from mcp-json-tool-result.json calls list_issues with the value at /repository and writes the response to /issueResponse:

{
"@call": "callMcpTool",
"catalogId": "https://a2ui.org/specification/v1_0/catalogs/mcp/catalog.json",
"args": {
"name": "list_issues",
"arguments": {"repository": {"@path": "/repository"}},
"targetDataPath": "/issueResponse"
}
}

Components on the same surface ("version": "1.0") can then bind to /issueResponse/structuredContent/issues (when the server returns structured content), /issueResponse/json (when the tool returns JSON text), or /issueResponse/text (for plain text).

Response object fields, error handling, and rapid-input protection

callMcpTool writes (and returns) an object with these fields:

FieldContents
structuredContentStructured data object returned by the MCP server, when the server includes one.
jsonParsed JSON value when text is a JSON object or array (or an array of parsed JSON blocks when the tool returns several text blocks).
textAll text content blocks from the tool result, joined with \n.
toolNameName of the tool that ran.
toolInputResolved arguments sent to the tool.
toolResultRaw MCP CallToolResult from the server.

If a tool call fails or returns isError: true, callMcpTool throws an A2uiExpressionError and leaves the data model unchanged.

When several calls write to the same targetDataPath concurrently (such as a search box calling a tool as the user types), only the latest call writes its result. Slower responses from earlier calls are ignored so stale results never overwrite newer ones. Give each independent UI feature its own targetDataPath so unrelated calls do not cancel each other’s writes.

Filter and reshape JSON results

When an MCP tool returns more fields or rows than your UI needs, chain callMcpTool with transformData (a JMESPath query) and updateDataModel:

{
"@call": "updateDataModel",
"catalogId": "https://a2ui.org/specification/v1_0/catalogs/mcp/catalog.json",
"args": {
"updates": {
"@call": "transformData",
"catalogId": "https://a2ui.org/specification/v1_0/catalogs/mcp/catalog.json",
"args": {
"jmespath": "{\"/issues\": json[?state == 'open'].{number: to_string(number), title: title}}",
"data": {
"@call": "callMcpTool",
"catalogId": "https://a2ui.org/specification/v1_0/catalogs/mcp/catalog.json",
"args": {
"name": "list_issues",
"arguments": {"repository": {"@path": "/repository"}}
}
}
}
}
}
}

Reading from the innermost call outward:

  1. callMcpTool runs list_issues and returns the response object containing json.
  2. transformData keeps only open issues, selects {number, title}, and wraps the result in {"/issues": [...]}.
  3. updateDataModel writes /issues into the surface data model, updating any components bound to /issues.

Note that the rapid-input guard only applies to targetDataPath on callMcpTool, not to updateDataModel. For inputs that fire on every keystroke, bind directly to targetDataPath.

Parse plain-text tool output

Some MCP servers return plain text meant for a language model (such as Title: ...\nURL: ... blocks) rather than JSON. The catalog includes split and regexCapture so an A2UI payload can extract rows and fields before passing them to transformData and updateDataModel, without adding per-tool parsing code to your host app. Your host app still chooses which MCP servers to connect through getMcpClientForTool (see Quick setup). These built-in functions are most useful when users or admins configure that server list at runtime, such as in a chat app or playground. If your app only connects to one or two MCP servers that you control, writing a custom function in your own catalog is usually simpler (see Add your own data-parsing functions under Advanced configuration and troubleshooting).

Here are the four data functions in @a2ui/catalog-mcp:

FunctionArgumentsWhat it does
transformDatajmespath, dataEvaluates a standard JMESPath query against data (returning null for missing fields).
updateDataModelupdatesWrites each {path: value} pair in updates to the surface data model (/ keys are absolute; others resolve relative to context).
splitvalue, separatorSplits a string (or each item in an array of strings) by separator, or into individual characters when separator is "".
regexCapturevalue, patternReturns capture groups from the first match of pattern (or null if unmatched) for a string or each string in an array.
Example: parsing plain-text search results

In mcp-text-tool-result.json, a search tool returns text records separated by blank lines (\n\n), where each record starts with Title: ... and URL: .... This chain splits the text into records, captures the title and URL from each record, drops non-matching header blocks ([?@ != null]), and writes the resulting array to /results:

{
"@call": "updateDataModel",
"catalogId": "https://a2ui.org/specification/v1_0/catalogs/mcp/catalog.json",
"args": {
"updates": {
"@call": "transformData",
"catalogId": "https://a2ui.org/specification/v1_0/catalogs/mcp/catalog.json",
"args": {
"jmespath": "{\"/results\": [?@ != null].{title: [0], url: [1]}}",
"data": {
"@call": "regexCapture",
"catalogId": "https://a2ui.org/specification/v1_0/catalogs/mcp/catalog.json",
"args": {
"pattern": "^Title: (.*)\nURL: (.*)",
"value": {
"@call": "split",
"catalogId": "https://a2ui.org/specification/v1_0/catalogs/mcp/catalog.json",
"args": {
"separator": "\n\n",
"value": {
"@call": "transformData",
"catalogId": "https://a2ui.org/specification/v1_0/catalogs/mcp/catalog.json",
"args": {
"jmespath": "text",
"data": {
"@call": "callMcpTool",
"catalogId": "https://a2ui.org/specification/v1_0/catalogs/mcp/catalog.json",
"args": {
"name": "tavily-search",
"arguments": {"query": {"@path": "/query"}}
}
}
}
}
}
}
}
}
}
}
}
}

Tools that return A2UI surfaces

MCP servers built specifically for A2UI can attach application/a2ui+json UI resources to their tools or tool responses. When callMcpTool runs one of those tools, it fetches the UI resource and forwards the A2UI messages to the processor you passed to configureMcpCatalog. To build an MCP server that serves A2UI layouts, see Serve A2UI Layouts from an MCP Server.


Advanced configuration and troubleshooting

Expand any section below for multi-server routing, partial catalog registration, custom parsers, regular expression rules, and troubleshooting fixes:

Route tool calls across multiple MCP servers (configureMcpCatalog options)

A2UI payloads refer to tools by name only and never hardcode server URLs. If your host connects to multiple MCP servers, discover each server’s tools with listTools() at startup and route each toolName to the matching client inside getMcpClientForTool (which may also return a Promise):

configureMcpCatalog({
getMcpClientForTool: toolName => clientsByTool.get(toolName) ?? defaultClient,
processor,
});

You can call configureMcpCatalog again at any time to update individual options:

OptionTypePurpose
getMcpClientForTool(toolName: string) => McpToolClient | Promise<...>Returns the MCP client (Pick<Client, 'request' | 'readResource' | 'listTools'>) that handles toolName.
processorMessageProcessorReceives A2UI messages extracted from tool results and application/a2ui+json UI resources.
defaultVersionstringProtocol version stamped onto decoded tool-result messages that omit version (defaults to '1.0').
onMcpAppResource(payload: McpAppResourcePayload, context: DataContext) => voidOptional callback invoked when callMcpTool resolves a ui:// (text/html;profile=mcp-app) resource.
sandboxPartial<SandboxConfig>Overrides where /a2ui-sandbox/ is hosted (baseUrl, default '/a2ui-sandbox/') or its startup check (disableSecuritySelfTest).

If you serve the sandbox directory at a custom path or on a dedicated sandbox origin, set sandbox.baseUrl before rendering the first surface:

configureMcpCatalog({sandbox: {baseUrl: '/static/frames/'}});

If getMcpClientForTool is not configured, McpApp dispatches allowed tool calls from the embedded widget as A2UI actions named after the tool, so a backend proxy agent can handle them through the MessageProcessor action callback.

Register only part of the catalog

If you want to expose McpApp and callMcpTool without the four data functions (or pick specific functions), derive a restricted catalog with mcpCatalog.withEntries and the individual *Implementation exports (CallMcpToolImplementation, TransformDataImplementation, UpdateDataModelImplementation, SplitImplementation, and RegexCaptureImplementation):

import {CallMcpToolImplementation, mcpCatalog} from '@a2ui/catalog-mcp';
const mcpAppOnlyCatalog = mcpCatalog.withEntries(
[...mcpCatalog.components.values()],
[CallMcpToolImplementation],
);
Add your own data-parsing functions

Every function in @a2ui/catalog-mcp must be implemented across every A2UI renderer, so the built-in function list stays small. If you need a specialized parser such as CSV, define it with createFunctionImplementation in your own catalog and register that catalog alongside mcpCatalog:

import {z} from 'zod';
import {mcpCatalog} from '@a2ui/catalog-mcp';
import {
basicCatalog,
Catalog,
createFunctionImplementation,
DynamicStringSchema,
MessageProcessor,
} from '@a2ui/web_core/v1_0';
const ParseCsvImplementation = createFunctionImplementation(
{
name: 'parseCsv',
returnType: 'any',
schema: z.object({value: DynamicStringSchema}),
description: 'Splits CSV text into rows of cells.',
},
({value}) =>
String(value)
.split('\n')
.map(line => line.split(',')),
);
const parsersCatalog = new Catalog(
'https://example.com/catalogs/parsers.json',
'1.0',
[],
[ParseCsvImplementation],
);
const processor = new MessageProcessor([basicCatalog, mcpCatalog, parsersCatalog], onAction);

Payloads can then call parseCsv with "catalogId": "https://example.com/catalogs/parsers.json" and compose it with callMcpTool and transformData.

Regular expression rules in regexCapture

To keep payloads portable across web and native renderers, regexCapture accepts JavaScript regular expression syntax without lookahead or lookbehind assertions ((?=, (?!, (?<=, (?<!) and without backreferences (\1 through \9, \k<name>). It also rejects patterns vulnerable to catastrophic backtracking, using the same safety check as the basic catalog’s regex function. Unsupported or unsafe patterns throw an A2uiExpressionError.

Troubleshooting duplicate Lit or @a2ui/web_core packages

Loading two copies of lit or @a2ui/web_core breaks custom element registration and Catalog instance checks. Run npm dedupe (and check npm ls lit @a2ui/web_core), and in Vite add resolve: {dedupe: ['lit', '@a2ui/web_core']} to vite.config.ts.


Next steps

  1. Configure /a2ui-sandbox/, run the Kanban board showcase, and explore double-iframe isolation in Embed MCP Apps in A2UI Surfaces.
  2. Build an MCP server that returns reusable A2UI layout templates in Serve A2UI Layouts from an MCP Server.
  3. Bundle an A2UI renderer inside a standalone ui:// widget for external MCP hosts in Render A2UI Inside an MCP App.
  4. Browse the TypeScript API reference in the @a2ui/catalog-mcp README and the MCP catalog JSON Schema.

Catalog schema

mcp_catalog.json

A2UI MCP Catalog — A2UI catalog for Model Context Protocol (MCP) integrations. It defines the `callMcpTool` function, the data functions that turn a tool result into data model updates, and the `McpApp` component that renders a sandboxed MCP App connected to the host through the MCP Apps bridge.

Source: catalogs/mcp/v1/catalog.json ↗ Raw JSON ↗ Permalink$id: https://a2ui.org/specification/v1_0/catalogs/mcp/catalog.json
Definitions & properties (8)
mcp_catalog.json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://a2ui.org/specification/v1_0/catalogs/mcp/catalog.json",
"protocolVersion": "1.0",
"title": "A2UI MCP Catalog",
"description": "A2UI catalog for Model Context Protocol (MCP) integrations. It defines the `callMcpTool` function, the data functions that turn a tool result into data model updates, and the `McpApp` component that renders a sandboxed MCP App connected to the host through the MCP Apps bridge.",
"catalogId": "https://a2ui.org/specification/v1_0/catalogs/mcp/catalog.json",
"components": {
"McpApp": {
"type": "object",
"description": "Renders a sandboxed Model Context Protocol application using double-iframe isolation. The application talks to the host over the MCP Apps JSON-RPC bridge; tool calls it is allowed to make are dispatched as A2UI actions.",
"properties": {
"component": {
"const": "McpApp"
},
"htmlContent": {
"$ref": "common_types.json#/$defs/DynamicString",
"description": "The HTML content of the app, rendered via srcdoc inside the sandbox. A value prefixed with `url_encoded:` is decoded with decodeURIComponent before rendering."
},
"title": {
"$ref": "common_types.json#/$defs/DynamicString",
"description": "The title of the app, used as the accessible title of the frame."
},
"allowedTools": {
"type": "array",
"description": "The names of the MCP tools the app is allowed to call. The host dispatches an authorized `tools/call` request as an A2UI action named after the tool, with the tool arguments as its context, and rejects any other tool call.",
"items": {
"type": "string"
}
},
"allowedFunctions": {
"type": "object",
"description": "A map of authorized host client function names to JSON Schemas describing their expected arguments. The host rejects a `ui/requests/function-call` request whose function is not listed or whose arguments fail validation. The functions must exist in the surface's catalog.",
"additionalProperties": {
"type": "object",
"description": "A JSON Schema. The host validates the matching payload against it before acting on the message; an empty object accepts any value."
}
},
"data": {
"type": "object",
"description": "Data binding configuration for the component. Reserved keys `toolInput`, `toolResult`, and `modelContext` map to standard MCP Apps `ui/notifications/tool-input`, `ui/notifications/tool-result`, and `ui/update-model-context` messages.",
"properties": {
"paths": {
"type": "object",
"description": "A dictionary mapping custom state keys (or reserved keys `toolInput`, `toolResult`, `modelContext`) to distinct JSON Pointer paths in the data model. The host pushes a `ui/notifications/data-model-update` notification whenever a bound value changes and writes authorized `ui/notifications/data-model-change` notifications back to the path.",
"additionalProperties": {
"type": "string"
}
}
},
"required": ["paths"],
"additionalProperties": false
},
"csp": {
"type": "object",
"description": "Per-resource Content Security Policy domain allowlists from `_meta.ui.csp`.",
"properties": {
"connectDomains": {
"type": "array",
"description": "Origins allowed in `connect-src` (fetch, XHR, WebSocket).",
"items": {
"type": "string"
}
},
"resourceDomains": {
"type": "array",
"description": "Origins allowed for scripts, styles, images, fonts, and media.",
"items": {
"type": "string"
}
},
"frameDomains": {
"type": "array",
"description": "Origins allowed for nested iframes (`frame-src`).",
"items": {
"type": "string"
}
},
"baseUriDomains": {
"type": "array",
"description": "Origins allowed for document `base-uri`.",
"items": {
"type": "string"
}
}
},
"additionalProperties": false
},
"permissions": {
"type": "object",
"description": "Browser capability permissions delegated to the inner sandboxed iframe from `_meta.ui.permissions`.",
"properties": {
"camera": {
"type": ["boolean", "object"],
"description": "Delegates `camera` access via the iframe `allow` attribute."
},
"microphone": {
"type": ["boolean", "object"],
"description": "Delegates `microphone` access via the iframe `allow` attribute."
},
"geolocation": {
"type": ["boolean", "object"],
"description": "Delegates `geolocation` access via the iframe `allow` attribute."
},
"clipboardWrite": {
"type": ["boolean", "object"],
"description": "Delegates `clipboard-write` access via the iframe `allow` attribute."
}
},
"additionalProperties": false
}
},
"required": ["component", "htmlContent"]
}
},
"functions": {
"callMcpTool": {
"type": "object",
"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; otherwise, for a result with several text blocks, the array of the parsed values of the blocks that are JSON, absent when none is), 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": {
"const": "callMcpTool"
},
"args": {
"type": "object",
"properties": {
"name": {
"$ref": "common_types.json#/$defs/DynamicString",
"description": "The name of the MCP tool to execute."
},
"arguments": {
"type": "object",
"description": "The arguments to pass to the MCP tool: an object of dynamic values, or a function call that returns one.",
"additionalProperties": {
"$ref": "common_types.json#/$defs/DynamicValue"
}
},
"targetDataPath": {
"$ref": "common_types.json#/$defs/DynamicString",
"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"],
"additionalProperties": false
}
},
"required": ["@call", "args"]
},
"transformData": {
"type": "object",
"description": "Evaluates a JMESPath expression (https://jmespath.org/specification.html) against `data`. Standard JMESPath only, no extensions.",
"returnType": "any",
"properties": {
"@call": {
"const": "transformData"
},
"args": {
"type": "object",
"properties": {
"jmespath": {
"$ref": "common_types.json#/$defs/DynamicString",
"description": "A JMESPath expression (https://jmespath.org/specification.html) evaluated against `data`. Standard JMESPath only, no extensions."
},
"data": {
"$ref": "common_types.json#/$defs/DynamicValue",
"description": "The input data to evaluate the expression against."
}
},
"required": ["jmespath", "data"],
"additionalProperties": false
}
},
"required": ["@call", "args"]
},
"updateDataModel": {
"type": "object",
"description": "Writes key-value pairs to the surface data model.",
"returnType": "any",
"properties": {
"@call": {
"const": "updateDataModel"
},
"args": {
"type": "object",
"properties": {
"updates": {
"$ref": "common_types.json#/$defs/DynamicValue",
"description": "An object mapping data model paths to the values to write (e.g., {\"/entries\": [...], \"/title\": \"Home\"}). Paths starting with \"/\" are absolute; relative paths resolve against the current data context."
}
},
"required": ["updates"],
"additionalProperties": false
}
},
"required": ["@call", "args"]
},
"split": {
"type": "object",
"description": "Splits a string into an array of substrings using a literal separator.",
"returnType": "any",
"properties": {
"@call": {
"const": "split"
},
"args": {
"type": "object",
"properties": {
"value": {
"$ref": "common_types.json#/$defs/DynamicValue",
"description": "The string or array of strings to split."
},
"separator": {
"$ref": "common_types.json#/$defs/DynamicString",
"description": "The delimiter string to split on. An empty string splits into individual characters (Unicode code points)."
}
},
"required": ["value", "separator"],
"additionalProperties": false
}
},
"required": ["@call", "args"]
},
"regexCapture": {
"type": "object",
"description": "Extracts capture groups from the first regular expression match.",
"returnType": "any",
"properties": {
"@call": {
"const": "regexCapture"
},
"args": {
"type": "object",
"properties": {
"value": {
"$ref": "common_types.json#/$defs/DynamicValue",
"description": "The string or array of strings to match against."
},
"pattern": {
"$ref": "common_types.json#/$defs/DynamicString",
"description": "A regular expression in the A2UI regex dialect: a portable subset of ECMAScript pattern syntax in Unicode mode with no flags. Returns an array of capture groups from the first match, or null if there is no match. Unmatched optional groups return empty strings."
}
},
"required": ["value", "pattern"],
"additionalProperties": false
}
},
"required": ["@call", "args"]
}
},
"$defs": {
"anyComponent": {
"oneOf": [
{
"$ref": "#/components/McpApp"
}
],
"discriminator": {
"propertyName": "component"
}
},
"anyFunction": {
"oneOf": [
{
"$ref": "#/functions/callMcpTool"
},
{
"$ref": "#/functions/transformData"
},
{
"$ref": "#/functions/updateDataModel"
},
{
"$ref": "#/functions/split"
},
{
"$ref": "#/functions/regexCapture"
}
]
}
}
}