diff --git a/catalogs/mcp/README.md b/catalogs/mcp/README.md index beeeaf35dd..532189f0a9 100644 --- a/catalogs/mcp/README.md +++ b/catalogs/mcp/README.md @@ -115,3 +115,5 @@ The [v1/examples](v1/examples/) directory holds A2UI message sequences that vali | Language | Package | | :--------- | :---------------------------------------------------- | | TypeScript | [`@a2ui/catalog-mcp`](../../typescript/catalogs/mcp/) | + +The [MCP catalog guide](../../docs/site/src/content/docs/guides/mcp-catalog.mdx) covers installing the package, serving the sandbox proxy that `McpApp` loads, and registering the catalog with a web renderer. diff --git a/docs/site/SITE_OUTLINE.md b/docs/site/SITE_OUTLINE.md index 4636faaa83..8b1352cf97 100644 --- a/docs/site/SITE_OUTLINE.md +++ b/docs/site/SITE_OUTLINE.md @@ -64,8 +64,9 @@ All cross-page links must target these exact Starlight routes (with `docs/site/s | **Guides** | How to Define a Catalog | **P1** | `content/docs/guides/defining-a-catalog.mdx` | `/guides/defining-a-catalog/` | | **Guides** | Architecting an A2UI Integration | **P2** | `content/docs/guides/architecting-an-integration.mdx` | `/guides/architecting-an-integration/` | | **Guides** | How to Improve Latency | **P2** | `content/docs/guides/improving-latency.mdx` | `/guides/improving-latency/` | +| **Guides** | MCP Catalog | **P1** | `content/docs/guides/mcp-catalog.mdx` | `/guides/mcp-catalog/` | | **Guides** | A2UI over MCP | **P1** | `content/docs/guides/a2ui-over-mcp.md` | `/guides/a2ui-over-mcp/` | -| **Guides** | MCP Apps in A2UI Surfaces | **P1** | `content/docs/guides/mcp-apps-in-a2ui.md` | `/guides/mcp-apps-in-a2ui/` | +| **Guides** | MCP Apps in A2UI Surfaces | **P1** | `content/docs/guides/mcp-apps-in-a2ui.mdx` | `/guides/mcp-apps-in-a2ui/` | | **Guides** | A2UI Dynamic Rendering within MCP Applications | **P1** | `content/docs/guides/a2ui-in-mcp-apps.md` | `/guides/a2ui-in-mcp-apps/` | | **Guides** | Authoring Universal Web Components | **P1** | `content/docs/guides/authoring-universal-components.md` | `/guides/authoring-universal-components/` | | **Guides** | SDK Development | **P2** | `content/docs/guides/sdk-development.md` | `/guides/sdk-development/` | @@ -366,6 +367,16 @@ _(Where `` is `v1.0`, `v0.9.1`, or `v0.8`.)_ 5. **Lever 4 — Tuning Thinking Budget**: Setting `thinking_budget = 0` for structured catalog UI generation to eliminate pre-first-token thinking delay. 6. **Lever 5 — Unbuffered Transport Flushing**: Disabling proxy buffering and feeding messages incrementally into `MessageProcessor` (`processMessages` / `process_messages` / `process(message:)`). + + +#### MCP Catalog (`/guides/mcp-catalog/`) [P1] + +- **Sources**: [`typescript/catalogs/mcp/README.md`](../../typescript/catalogs/mcp/README.md), [`catalogs/mcp/README.md`](../../catalogs/mcp/README.md), [`catalogs/mcp/v1/catalog.json`](../../catalogs/mcp/v1/catalog.json), [`catalogs/mcp/v1/examples/`](../../catalogs/mcp/v1/examples/), and `/specification/v1.0/schemas/#mcp_catalog.json`. +- **Content Outline**: + 1. **Overview & Quick Setup**: Installing `@a2ui/catalog-mcp`, connecting an MCP `Client` and registering `mcpCatalog` across Lit, React, and Angular (`configureMcpCatalog`), and sharing `catalog.json` with the agent. + 2. **Calling MCP Tools from UI Surfaces**: Storing output directly via `targetDataPath` (with response-object details and rapid-input protection in an expandable section), filtering JSON with `transformData` and `updateDataModel`, and parsing plain-text output with `split` and `regexCapture`. + 3. **Embedding Sandboxed `McpApp` Widgets & Advanced Config**: Serving `/a2ui-sandbox/`, adding and styling `McpApp`, and expandable sections for `McpApp` property reference, multi-server routing, custom parsers, and troubleshooting. + #### A2UI over MCP (`/guides/a2ui-over-mcp/`) [P1] @@ -376,7 +387,7 @@ _(Where `` is `v1.0`, `v0.9.1`, or `v0.8`.)_ #### MCP Apps in A2UI Surfaces (`/guides/mcp-apps-in-a2ui/`) [P1] -- Port [`docs/public/guides/mcp-apps-in-a2ui.md`](../public/guides/mcp-apps-in-a2ui.md) (Double-Iframe Isolation Pattern). +- Companion runnable-samples and architecture guide in [`src/content/docs/guides/mcp-apps-in-a2ui.mdx`](src/content/docs/guides/mcp-apps-in-a2ui.mdx), leading with a quick start for the v1.0 Kanban showcase ([`samples/community/mcp/a2ui-kanban/`](../../samples/community/mcp/a2ui-kanban/)) followed by `McpApp` `sandbox="allow-scripts"` double-iframe isolation ([`catalogs/mcp/v1/mcp_app_specification.md`](../../catalogs/mcp/v1/mcp_app_specification.md)) and expandable JSON-RPC 2.0 channel details. diff --git a/docs/site/astro.config.mjs b/docs/site/astro.config.mjs index 9d372e505b..a1e9aa28ac 100644 --- a/docs/site/astro.config.mjs +++ b/docs/site/astro.config.mjs @@ -62,6 +62,13 @@ export default defineConfig({ href: 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap', }, }, + { + tag: 'script', + attrs: { + type: 'module', + }, + content: `const renderMermaid = async () => { for (const pre of document.querySelectorAll('pre[data-language="mermaid"]')) { const lines = pre.querySelectorAll('.ec-line'); const code = (lines.length ? Array.from(lines, l => l.textContent).join('\n') : (pre.querySelector('code')?.textContent || pre.textContent)).trim(); const div = document.createElement('div'); div.className = 'mermaid-rendered not-content'; div.dataset.mermaidSource = code; (pre.closest('.expressive-code') || pre).replaceWith(div); } const blocks = document.querySelectorAll('.mermaid-rendered[data-mermaid-source]'); if (!blocks.length) return; const { default: mermaid } = await import('https://cdn.jsdelivr.net/npm/mermaid@11.4.1/dist/mermaid.esm.min.mjs'); const isDark = document.documentElement.getAttribute('data-theme') !== 'light'; mermaid.initialize({ startOnLoad: false, theme: isDark ? 'dark' : 'default', fontFamily: 'inherit' }); for (let i = 0; i < blocks.length; i++) { const div = blocks[i]; const code = div.dataset.mermaidSource; if (!code) continue; try { const id = 'mermaid-svg-' + i + '-' + Date.now().toString(36); const { svg } = await mermaid.render(id, code); div.innerHTML = svg; } catch (err) { console.error('Mermaid render error:', err); } } }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', renderMermaid); } else { renderMermaid(); } window.addEventListener('theme-changed', renderMermaid);`, + }, ], components: { Header: './src/components/StarlightHeader.astro', @@ -143,6 +150,10 @@ export default defineConfig({ label: 'How to Define a Catalog', slug: 'guides/defining-a-catalog', }, + { + label: 'MCP Catalog', + slug: 'guides/mcp-catalog', + }, { label: 'A2UI over MCP', slug: 'guides/a2ui-over-mcp', diff --git a/docs/site/src/content/docs/guides/mcp-apps-in-a2ui.md b/docs/site/src/content/docs/guides/mcp-apps-in-a2ui.md deleted file mode 100644 index 066246560f..0000000000 --- a/docs/site/src/content/docs/guides/mcp-apps-in-a2ui.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: "MCP Apps in A2UI Surfaces" -description: "Embedding sandboxed MCP application surfaces inside A2UI interfaces." ---- - -:::note[Work in Progress] -This page is currently being authored. Refer to the site outline in the repository for the specification and planned content. -::: diff --git a/docs/site/src/content/docs/guides/mcp-apps-in-a2ui.mdx b/docs/site/src/content/docs/guides/mcp-apps-in-a2ui.mdx new file mode 100644 index 0000000000..984f34f7ed --- /dev/null +++ b/docs/site/src/content/docs/guides/mcp-apps-in-a2ui.mdx @@ -0,0 +1,295 @@ +--- +title: 'MCP Apps in A2UI Surfaces' +description: 'Run interactive MCP App widgets inside an A2UI surface and see how double-iframe isolation and state synchronization work.' +--- + +import {Aside} from '@astrojs/starlight/components'; + +export const baseUrl = import.meta.env.BASE_URL.replace(/\/$/, ''); + +The `McpApp` component in `@a2ui/catalog-mcp` embeds interactive **[Model Context Protocol (MCP) Apps](https://github.com/modelcontextprotocol/ext-apps)** inside an A2UI surface so third-party HTML widgets, native A2UI components, and your agent stay in sync. + +Standard A2UI components cover forms, cards, and lists, while MCP Apps let an MCP server ship a custom HTML widget, such as a drag-and-drop Kanban board or a 3D canvas, alongside its tools. When you place `McpApp` on a surface, `@a2ui/catalog-mcp` runs that HTML inside an isolated sandbox and connects its state and tool calls to the surrounding UI. + + + +MCP calculator demo showing the host loading the app, opening the calculator, and chatting with the agent + +--- + +## Quick start: run the Kanban showcase + +The fastest way to see an MCP App working inside an A2UI v1.0 surface is to run the Kanban board showcase ([`samples/community/mcp/a2ui-kanban/`](https://github.com/a2ui-project/a2ui/blob/main/samples/community/mcp/a2ui-kanban/README.md)). It connects three local processes: an MCP server that provides a drag-and-drop board widget (`ui://kanban/board`), a Python agent, and a Lit chat app built with `basicCatalog` and `mcpCatalog`. + +### Prerequisites + +Install these tools before running the sample from a clone of the A2UI repository: + +1. Python 3.10 or later for the agent backend. +2. [`uv`](https://docs.astral.sh/uv/) to install Python dependencies and start the agent. +3. Node.js 18 or later with Corepack enabled (`corepack enable`) so `yarn` uses the repository's configured workspace manager. +4. A `GEMINI_API_KEY` from [Google AI Studio](https://aistudio.google.com/apikey), or `KANBAN_AGENT=scripted` to try the guided walkthrough without an API key. + +### Start the three services + +Open three terminals starting from the root of the A2UI repository. + +#### Build the packages and start the MCP server + +In the first terminal, build `@a2ui/catalog-mcp` and `@a2ui/lit`, install the `samples/community` dependencies, and start the Kanban MCP server on `http://localhost:8788/mcp`: + +```bash +yarn install +yarn workspace @a2ui/catalog-mcp run build +yarn workspace @a2ui/lit run build + +cd samples/community +yarn install + +cd mcp/a2ui-kanban/mcp-server +yarn build +yarn start +``` + +#### Start the Kanban agent + +In the second terminal, start the Python A2A agent on `http://localhost:10007`: + +```bash +cd samples/community/mcp/a2ui-kanban/agent +export GEMINI_API_KEY="your-api-key" # or: export KANBAN_AGENT=scripted +uv run . +``` + +#### Start the chat app + +In the third terminal, start the Vite dev server on `http://localhost:5173`: + +```bash +cd samples/community/mcp/a2ui-kanban/client +yarn dev +``` + +### Things to try in the browser + +Open `http://localhost:5173/` and try these interactions to see the widget, the A2UI surface, and the agent working together: + +1. Click **"Show me the sprint board"** to render the sandboxed Kanban widget alongside native A2UI summary badges and a quick-add task bar. +2. Drag a card across columns inside the widget and watch the sibling A2UI column counts update right away without waiting for an agent turn. +3. Enter a title and assignee in the A2UI quick-add bar and click **"Add task"** to call the MCP server from the browser and update both the board widget and the badges at once. +4. Click a card to select it, then ask the agent in chat to split or move that card using the selection context shared by the widget. +5. Open the **Protocol Debug Inspector** drawer on the right side of the page to inspect the live A2UI messages, `postMessage` frames, and MCP tool calls behind each action. + +
+Legacy pre-v1.0 samples (Calculator, Pong, and standalone Lit) + +The two earlier samples below predate A2UI v1.0 and `@a2ui/catalog-mcp` (the Calculator and Pong sample uses protocol v0.9 and the standalone Lit sample uses protocol v0.8). Each defines its own sample-local custom component rather than importing `McpApp` from `@a2ui/catalog-mcp`. + +#### Calculator and Pong (Angular v0.9 + proxy agent + MCP server) + +This sample runs an SSE Calculator MCP server, a Python ADK proxy agent, and an Angular client hosting calculator and Pong widgets in separate sandboxed iframes: + +1. Start the Calculator MCP server on `http://localhost:8000` (pass `--port 8001` if port `8000` is in use): + + ```bash + cd samples/community/mcp/mcp-apps-calculator/ + uv run . + ``` + +2. In a second terminal, start the MCP proxy agent on `http://localhost:10006`: + + ```bash + cd samples/community/agent/adk/mcp_app_proxy/ + export GEMINI_API_KEY="your-api-key" + uv run . + ``` + +3. Build the monorepo renderer packages at the repository root, then install and start the Angular client on `http://localhost:4200`: + + ```bash + yarn build:all + cd samples/community/client/angular/ + yarn install + yarn start mcp_calculator + ``` + +4. Open `http://localhost:4200/?disable_security_self_test=true` in your browser. Because this pre-v1.0 sample serves its proxy iframe on the same `localhost` origin without `@a2ui/catalog-mcp`'s automatic origin check, pass `?disable_security_self_test=true` in the URL to skip the cross-origin self-test during local development: + + + + + + + + + + + + + + +
Calculator AppPong App
+ Animated GIF of the calculator app performing multiplications. + + Animated GIF of the Pong app updating a sibling A2UI scoreboard. +
+ +#### Standalone Lit MCP App sample (Lit v0.8 + ADK agent) + +This minimal two-process sample pairs an ADK agent with a Lit client to test button actions inside a sandboxed iframe: + +1. Start the ADK agent on `http://localhost:8000`: + + ```bash + cd samples/community/agent/adk/mcp-apps-in-a2ui-sample + export GEMINI_API_KEY="your-api-key" + uv run agent.py + ``` + +2. In a second terminal, install dependencies and start the Lit client on `http://localhost:5173`: + + ```bash + cd samples/community/client/lit/mcp-apps-in-a2ui-sample + yarn install + yarn dev + ``` + +3. Open `http://localhost:5173/` and click **"Call Agent Tool"** inside the iframe to send a tool action through the host to the agent. + +
+ +
+Troubleshooting the samples + +If a sample fails to build or load, check these common issues: + +| Symptom | Fix | +| :---------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------- | +| `GEMINI_API_KEY environment variable not set` | Export `GEMINI_API_KEY` in that terminal, add a `.env` file in the agent directory, or set `KANBAN_AGENT=scripted` for the Kanban agent. | +| Kanban client fails to compile `@a2ui/catalog-mcp` or `@a2ui/lit` | Run `yarn install`, `yarn workspace @a2ui/catalog-mcp run build`, and `yarn workspace @a2ui/lit run build` at the repository root first. | +| Port `8788` or `10007` is already in use on Kanban | Start the service on another port and pass `?mcp_url=http://localhost:/mcp&agent_url=http://localhost:` to `http://localhost:5173/`. | +| Angular client shows a blank page or missing renderer imports | Run `yarn build:all` at the repository root before running `yarn start mcp_calculator` in `samples/community/client/angular/`. | +| `ng serve` not found in the Angular sample | Run `yarn install` inside `samples/community/client/angular/` to install `@angular/cli`. | +| `"URL with hostname not allowed"` in Angular | Open `http://localhost:4200/` rather than `0.0.0.0` (Angular's dev server restricts allowed hosts by default). | +| Security self-test error in the legacy Angular sample | Append `?disable_security_self_test=true` to `http://localhost:4200/`. | + +
+ +--- + +## How McpApp works under the hood + +When you move beyond the samples and review `McpApp` for production, two design choices matter most: how untrusted HTML is isolated from your host app, and how state crosses that boundary safely. + +### Double-iframe sandbox + +Putting third-party HTML in a single iframe with both `allow-scripts` and `allow-same-origin` is unsafe because scripts inside that frame share your app's origin and can read cookies, web storage, or `window.parent.document`. + +Instead, `McpApp` uses two nested iframes. An outer proxy frame loads `/a2ui-sandbox/sandbox.html`, checks message origins, and injects the untrusted HTML into an inner `srcdoc` iframe with `sandbox="allow-scripts"` only. Without `allow-same-origin`, the browser assigns the inner frame a unique opaque origin (`null`), blocking access to your app's DOM, cookies, and storage. + +In the diagram below, the host web application renders an A2UI surface containing `McpApp` (`a2ui-mcp-app`). That element exchanges origin-checked `postMessage` events with the outer `/a2ui-sandbox/sandbox.html` proxy iframe, which relays JSON-RPC 2.0 messages to and from the inner `srcdoc` iframe running on an opaque origin with `sandbox='allow-scripts'`. + +```mermaid +flowchart TD + subgraph HostApp ["Host Web Application"] + A["A2UI surface"] --> B["McpApp (a2ui-mcp-app)"] + end + subgraph OuterProxy ["Outer Sandbox Proxy"] + B <-->|"Origin-checked postMessage"| C["iframe /a2ui-sandbox/sandbox.html"] + end + subgraph InnerFrame ["Embedded MCP App"] + C <-->|"JSON-RPC 2.0 relay"| D["iframe srcdoc (opaque origin, sandbox='allow-scripts')"] + end +``` + +
+Advanced: outer proxy checks, CSP allowlists, and authoring constraints + +Each `McpApp` instance enforces the following rules across its two frames: + +1. The outer frame loads `sandbox.html` from `/a2ui-sandbox/` on your app's origin, or from a dedicated sandbox origin set via `configureMcpCatalog({sandbox: {baseUrl}})`. When hosted on a separate origin, `sandbox.html` runs a startup self-test verifying that it cannot reach `window.top`; on the same origin, `@a2ui/catalog-mcp` disables that check automatically. +2. The inner frame receives `sandbox="allow-scripts"` only. Omitting `allow-same-origin` isolates cookies and storage, omitting `allow-top-navigation` and `allow-top-navigation-by-user-activation` prevents frame-busting redirects (`window.top.location = "..."`), and omitting `allow-popups`, `allow-forms`, and `allow-modals` blocks unprompted windows, form submissions, and modal dialogs. +3. Before injecting `srcdoc`, the outer proxy applies any Content Security Policy domain allowlists in `csp` (`_meta.ui.csp`) and browser capability flags in `permissions` (`_meta.ui.permissions`, such as `camera`, `microphone`, `geolocation`, and `clipboard-write`). + +If you are authoring an MCP App to run inside this sandbox, keep three browser restrictions in mind: + +1. `localStorage`, `sessionStorage`, `IndexedDB`, and cookies throw security errors inside an opaque `null` origin. Persist state on the MCP server via `tools/call` or synchronize it with the host surface via `data.paths`. +2. `window.parent.document` and `window.top` are blocked. Request iframe height updates with `ui/notifications/size-changed`, and open external `http:` or `https:` links in a new tab with `ui/open-link`. +3. External network requests, images, fonts, and subframes inside `srcdoc` are blocked unless the MCP resource lists their domains under `_meta.ui.csp` (`connectDomains`, `resourceDomains`, `frameDomains`, `baseUriDomains`). + +To serve `/a2ui-sandbox/` in your bundler and configure [`McpApp`](../../specification/v1.0/schemas/#mcp_catalog.json/components/McpApp) properties, see [Embedding interactive MCP Apps](../mcp-catalog/#embedding-interactive-mcp-apps) in the MCP Catalog guide. For the startup handshake (`ui/notifications/sandbox-proxy-ready`, `ui/notifications/sandbox-resource-ready`, and `ui/initialize`), see the [MCP App component specification](https://github.com/a2ui-project/a2ui/blob/main/catalogs/mcp/v1/mcp_app_specification.md). + +
+ +### Synchronizing state and tool calls + +Once the sandbox loads, `McpApp` bridges four communication paths over JSON-RPC 2.0: reactive two-way data paths shared with sibling A2UI components, direct MCP tool calls from the browser, local A2UI catalog function calls, and follow-up messages or context updates sent to the agent. + +In the diagram below, the host web application holds `MessageProcessor` and the surface data model alongside `McpApp` and its sibling A2UI components. `McpApp` synchronizes bound `data.paths` keys and `allowedFunctions` locally with `MessageProcessor`, calls allowed MCP tools directly via `getMcpClientForTool`, and forwards `a2ui.mcpAppMessage` actions and `modelContext` state to the AI agent, which can invoke MCP tools and stream `updateDataModel` or `updateComponents` messages back to `MessageProcessor`. + +```mermaid +flowchart TD + MCPServer["MCP Server (tools and ui:// HTML resources)"] + Agent["AI Agent (streams A2UI v1.0 messages)"] + + subgraph HostApp ["Host Web Application"] + direction TB + Processor["MessageProcessor and Surface Data Model"] + + subgraph SurfaceTree ["A2UI Surface"] + McpComponent["McpApp Component (double-iframe HTML widget)"] + Siblings["Sibling A2UI Components (Text, Button, TextField)"] + end + + McpComponent <-->|"1. Two-way data.paths and allowedFunctions"| Processor + Processor <-->|"Reactive @path and @call bindings"| Siblings + end + + McpComponent ==>|"2. Direct tools/call (getMcpClientForTool)"| MCPServer + Siblings ==>|"callMcpTool"| MCPServer + Processor ==>|"3. a2ui.mcpAppMessage and modelContext"| Agent + Agent <==>|"MCP tool calls"| MCPServer + Agent ==>|"4. updateDataModel or updateComponents"| Processor +``` + +
+Advanced: JSON-RPC 2.0 channels and how the Kanban showcase uses them + +`McpApp` communicates with the embedded widget using `@modelcontextprotocol/ext-apps/app-bridge` plus A2UI v1.0 extensions across four channels: + +1. Two-way local data binding (`data.paths`). When a bound path in the surface data model changes (from a sibling input, a client function, or an `updateDataModel` message from the agent), `McpApp` sends `ui/notifications/data-model-update` to the widget (plus standard `ui/notifications/tool-input` and `ui/notifications/tool-result` notifications when `toolInput` or `toolResult` are bound). When the widget emits `ui/notifications/data-model-change`, `McpApp` writes the new value to the bound path so sibling A2UI components update immediately. +2. Direct MCP tool execution (`tools/call`). When the widget calls a tool listed in `allowedTools` and the host registered `getMcpClientForTool` via `configureMcpCatalog`, `McpApp` calls the MCP server directly and returns the `CallToolResult`. If no browser MCP client is configured, `McpApp` dispatches an A2UI action named after the tool so a backend proxy agent can run it instead. Unlisted tool names are rejected with a JSON-RPC error. +3. Local catalog function calls (`ui/requests/function-call`). When `allowedFunctions` maps A2UI catalog function names to JSON Schemas for their arguments, the widget can validate arguments and invoke those functions locally on the surface. +4. Agent context and follow-up turns (`ui/update-model-context` and `ui/message`). Calling `ui/update-model-context` writes the widget's active selection or filter state to the bound `modelContext` path in the surface data model so your host can attach it to the next user turn. Calling `ui/message` dispatches an `a2ui.mcpAppMessage` action so the host can forward a prompt or event from the widget to the agent. + +In the Kanban showcase above, every interaction maps to one of these channels: + +1. Asking the agent to move or create tasks calls MCP tools on the server and streams `updateDataModel` to `/kanban/board`, which `McpApp` forwards via `ui/notifications/data-model-update`. +2. Selecting a card or filtering by assignee calls `ui/update-model-context`, writing `/kanban/modelContext` and `/kanban/selection` for the next chat turn. +3. Dragging a card sends `ui/notifications/data-model-change` for `board`, updating `/kanban/board` locally and recomputing the sibling `Text` column and WIP badges (bound via `@call: transformData`). +4. Dropping a card (`tools/call` plus `ui/message`) or clicking **"Ask agent to split"** (`ui/message`) dispatches `a2ui.mcpAppMessage` to the chat host. +5. Submitting the quick-add task bar chains `callMcpTool("create_task")`, `transformData`, and `updateDataModel` in the browser to update `/kanban/board` with zero agent turns. + +
+ +--- + +## Next steps + +1. Install and configure `@a2ui/catalog-mcp` in your own application in the [MCP Catalog](../mcp-catalog/) guide. +2. Review the handshake lifecycle, JSON-RPC message schemas, and cycle-prevention rules in the [MCP App component specification](https://github.com/a2ui-project/a2ui/blob/main/catalogs/mcp/v1/mcp_app_specification.md). +3. Build an MCP server that returns reusable A2UI component layouts in [A2UI over MCP](../a2ui-over-mcp/). +4. Render A2UI surfaces inside an MCP App in [A2UI Dynamic Rendering within MCP Applications](../a2ui-in-mcp-apps/). diff --git a/docs/site/src/content/docs/guides/mcp-catalog.mdx b/docs/site/src/content/docs/guides/mcp-catalog.mdx new file mode 100644 index 0000000000..e41ab06724 --- /dev/null +++ b/docs/site/src/content/docs/guides/mcp-catalog.mdx @@ -0,0 +1,534 @@ +--- +title: 'MCP Catalog' +description: 'Connect A2UI v1.0 web surfaces to Model Context Protocol (MCP) tools and sandboxed MCP App widgets with @a2ui/catalog-mcp.' +--- + +import {Tabs, TabItem} from '@astrojs/starlight/components'; + +The `@a2ui/catalog-mcp` package connects A2UI v1.0 or later web surfaces directly to **[Model Context Protocol (MCP)](https://modelcontextprotocol.io/)** servers, letting UI components call MCP tools and embed sandboxed [MCP Apps](https://github.com/modelcontextprotocol/ext-apps) without waiting for another round-trip to the agent. + +An MCP server exposes tools (functions like `list_issues` or `get_weather` that return data) and optional interactive HTML widgets. Once you register `mcpCatalog` in your Lit, React, or Angular app, your surfaces can: + +1. Call MCP tools straight from buttons and inputs, then display the returned JSON or text right away. +2. Render interactive MCP Apps (such as a 3D canvas or a Kanban board) inside an isolated sandbox alongside native A2UI components. + +--- + +## 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: + +```bash +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`: + + + + +```ts +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, +}); +``` + + + + +```ts +import {configureMcpCatalog, mcpCatalog} from '@a2ui/catalog-mcp'; +import {type ReactCatalogComponent} from '@a2ui/react'; +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, +}); +``` + + + + +```ts +import {ApplicationConfig, inject, provideEnvironmentInitializer} from '@angular/core'; +import {A2uiRendererService, AngularCatalog, BasicCatalog, provideA2UI} from '@a2ui/angular'; +import {configureMcpCatalog, mcpCatalog} from '@a2ui/catalog-mcp'; +import {Client} from '@modelcontextprotocol/sdk/client/index.js'; +import {StreamableHTTPClientTransport} from '@modelcontextprotocol/sdk/client/streamableHttp.js'; + +const mcpClient = new Client({name: 'my-web-app', version: '1.0.0'}); +await mcpClient.connect(new StreamableHTTPClientTransport(new URL('http://localhost:8000/mcp'))); + +const mcpAngularCatalog = new AngularCatalog( + mcpCatalog.id, + mcpCatalog.protocolVersion, + [...mcpCatalog.components.values()], + [...mcpCatalog.functions.values()], +); + +export const appConfig: ApplicationConfig = { + providers: [ + provideA2UI(() => ({ + catalogs: [inject(BasicCatalog), mcpAngularCatalog], + })), + provideEnvironmentInitializer(() => { + configureMcpCatalog({ + getMcpClientForTool: () => mcpClient, + processor: inject(A2uiRendererService).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](#advanced-configuration-and-troubleshooting). + +### Share the catalog schema with your agent + +Include the [MCP catalog schema](../../specification/v1.0/schemas/#mcp_catalog.json) 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`](https://github.com/a2ui-project/a2ui/blob/main/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 [Embedding interactive MCP Apps](#embedding-interactive-mcp-apps). + +--- + +## 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`](../../specification/v1.0/schemas/#mcp_catalog.json/functions/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`](https://github.com/a2ui-project/a2ui/blob/main/catalogs/mcp/v1/examples/mcp-json-tool-result.json) calls `list_issues` with the value at `/repository` and writes the response to `/issueResponse`: + +```json +{ + "@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: + +| Field | Contents | +| :------------------ | :------------------------------------------------------------------------------------------------------------------------------------- | +| `structuredContent` | Structured data object returned by the MCP server, when the server includes one. | +| `json` | Parsed JSON value when `text` is a JSON object or array (or an array of parsed JSON blocks when the tool returns several text blocks). | +| `text` | All `text` content blocks from the tool result, joined with `\n`. | +| `toolName` | Name of the tool that ran. | +| `toolInput` | Resolved arguments sent to the tool. | +| `toolResult` | Raw 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](https://jmespath.org/specification.html) query) and `updateDataModel`: + +```json +{ + "@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](#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](#advanced-configuration-and-troubleshooting)). + +Here are the four data functions in `@a2ui/catalog-mcp`: + +| Function | Arguments | What it does | +| :---------------- | :------------------- | :----------------------------------------------------------------------------------------------------------------------------------- | +| `transformData` | `jmespath`, `data` | Evaluates a standard [JMESPath](https://jmespath.org/specification.html) query against `data` (returning `null` for missing fields). | +| `updateDataModel` | `updates` | Writes each `{path: value}` pair in `updates` to the surface data model (`/` keys are absolute; others resolve relative to context). | +| `split` | `value`, `separator` | Splits a string (or each item in an array of strings) by `separator`, or into individual characters when `separator` is `""`. | +| `regexCapture` | `value`, `pattern` | Returns 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`](https://github.com/a2ui-project/a2ui/blob/main/catalogs/mcp/v1/examples/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`: + +```json +{ + "@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 [A2UI over MCP](../a2ui-over-mcp/). + +--- + +## Embedding interactive MCP Apps + +Some MCP servers provide interactive HTML widgets called [MCP Apps](https://github.com/modelcontextprotocol/ext-apps) for interfaces that go beyond standard form controls, such as a 3D canvas or a drag-and-drop Kanban board. The [`McpApp`](../../specification/v1.0/schemas/#mcp_catalog.json/components/McpApp) component runs those widgets inside a double-iframe sandbox and keeps their state in sync with your A2UI surface. + +### Serve the sandbox files + +`McpApp` loads its outer sandbox relay from `/a2ui-sandbox/` on your app's origin. Configure your bundler or web server to serve `node_modules/@a2ui/catalog-mcp/sandbox` at `/a2ui-sandbox/`: + + + + +Copy the folder into `public/a2ui-sandbox/`, or copy it at build time with [`vite-plugin-static-copy`](https://github.com/sapphi-red/vite-plugin-static-copy): + +```ts +import {defineConfig} from 'vite'; +import {viteStaticCopy} from 'vite-plugin-static-copy'; + +export default defineConfig({ + plugins: [ + viteStaticCopy({ + targets: [{src: 'node_modules/@a2ui/catalog-mcp/sandbox/*', dest: 'a2ui-sandbox'}], + }), + ], +}); +``` + + + + +Add the sandbox directory to the `assets` array in `angular.json`: + +```json +{ + "glob": "**/*", + "input": "node_modules/@a2ui/catalog-mcp/sandbox", + "output": "a2ui-sandbox" +} +``` + + + + +Copy `node_modules/@a2ui/catalog-mcp/sandbox` to `/a2ui-sandbox` on the same origin as your app. + + + + +### Add McpApp to a surface + +Place `McpApp` on an A2UI v1.0 surface with its HTML document (`htmlContent`), any surface data paths it shares (`data.paths`), and any MCP tools it is allowed to call (`allowedTools`): + +```json +{ + "id": "scene3d_app", + "component": "McpApp", + "catalogId": "https://a2ui.org/specification/v1_0/catalogs/mcp/catalog.json", + "title": "3D Holographic Geometry Studio", + "allowedTools": ["capture_3d_snapshot"], + "data": { + "paths": { + "scene3d": "/scene3d", + "modelContext": "/scene3d/modelContext" + } + }, + "htmlContent": "..." +} +``` + +In this component, updates to `/scene3d` on the surface push into the widget, edits to `scene3d` inside the widget update sibling A2UI components, and tool calls outside `["capture_3d_snapshot"]` are blocked. + +
+All McpApp component properties + +An embedded `McpApp` is isolated from your page DOM, cookies, and browser storage, and interacts with your surface only through explicit properties: + +| Property | Required | Purpose | +| :----------------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------- | +| `htmlContent` | Yes | HTML document of the app (values starting with `url_encoded:` are URL-decoded first). | +| `title` | No | Accessible title for the iframe (defaults to `"MCP App"`). | +| `data.paths` | No | Maps state keys inside the app (plus reserved `toolInput`, `toolResult`, and `modelContext` keys) to data model paths for two-way reactive sync. | +| `allowedTools` | No | List of MCP tool names the app is allowed to call. Unlisted tool calls are rejected. | +| `allowedFunctions` | No | Map of A2UI catalog function names to argument JSON Schemas that the app is allowed to invoke. | +| `csp` | No | Content Security Policy domain allowlists (`connectDomains`, `resourceDomains`, `frameDomains`, `baseUriDomains`) from the resource `_meta.ui.csp`. | +| `permissions` | No | Browser permissions (`camera`, `microphone`, `geolocation`, `clipboardWrite`) delegated from the resource `_meta.ui.permissions`. | + +When a tool result returned by `callMcpTool` contains an MCP App `ui://` resource, its `resourceUri`, `html`, `csp`, and `permissions` fields are also included in the `callMcpTool` payload so you can bind `McpApp` properties to them. + +
+ +### Style the frame + +`McpApp` starts at `500px` tall and resizes automatically when the embedded widget reports its content size. Customize the container with CSS custom properties on `a2ui-mcp-app` or any parent element: + +```css +a2ui-mcp-app { + --a2ui-sandboxed-frame-height: 400px; + --a2ui-sandboxed-frame-border: 1px solid #e2e8f0; + --a2ui-sandboxed-frame-border-radius: 12px; + --a2ui-sandboxed-frame-background: #ffffff; +} +``` + +
+Default values for CSS custom properties + +| CSS custom property | Default | Purpose | +| :------------------------------------- | :------------------------------------------------------------------- | :------------------------------------- | +| `--a2ui-sandboxed-frame-height` | `500px` | Initial height before auto-resize. | +| `--a2ui-sandboxed-frame-border` | `var(--a2ui-border-width, 1px) solid var(--a2ui-color-border, #ccc)` | Border around the frame. | +| `--a2ui-sandboxed-frame-border-radius` | `var(--a2ui-border-radius, 8px)` | Corner radius of the frame. | +| `--a2ui-sandboxed-frame-background` | `#fff` | Background behind transparent content. | + +
+ +--- + +## 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`): + +```ts +configureMcpCatalog({ + getMcpClientForTool: toolName => clientsByTool.get(toolName) ?? defaultClient, + processor, +}); +``` + +You can call `configureMcpCatalog` again at any time to update individual options: + +| Option | Type | Purpose | +| :-------------------- | :--------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------- | +| `getMcpClientForTool` | `(toolName: string) => McpToolClient \| Promise<...>` | Returns the MCP client (`Pick`) that handles `toolName`. | +| `processor` | `MessageProcessor` | Receives A2UI messages extracted from tool results and `application/a2ui+json` UI resources. | +| `defaultVersion` | `string` | Protocol version stamped onto decoded tool-result messages that omit `version` (defaults to `'1.0'`). | +| `onMcpAppResource` | `(payload: McpAppResourcePayload, context: DataContext) => void` | Optional callback invoked when `callMcpTool` resolves a `ui://` (`text/html;profile=mcp-app`) resource. | +| `sandbox` | `Partial` | 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: + +```ts +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`): + +```ts +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](../defining-a-catalog/) and register that catalog alongside `mcpCatalog`: + +```ts +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 (`(?=`, `(?!`, `(?<=`, `(?`). 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 blank frames, rejected calls, and duplicate Lit packages + +#### The McpApp frame stays blank (404 under /a2ui-sandbox/) + +Check that `node_modules/@a2ui/catalog-mcp/sandbox` is served at `/a2ui-sandbox/` on your app's origin (see [Serve the sandbox files](#serve-the-sandbox-files)), or pass `configureMcpCatalog({sandbox: {baseUrl}})` with the path where you host those files. + +#### Tool or function calls from an embedded McpApp are rejected + +Check the browser console for warnings. Every MCP tool the widget calls must be listed in `allowedTools`, and every A2UI function must be listed in `allowedFunctions` and registered in `mcpCatalog` or the surface's catalog. + +#### Custom element registration collisions or empty components + +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. Run the Kanban board showcase and see how double-iframe isolation and JSON-RPC channels work in [MCP Apps in A2UI Surfaces](../mcp-apps-in-a2ui/). +2. Build an MCP server that returns reusable A2UI layout templates in [A2UI over MCP](../a2ui-over-mcp/). +3. Browse the TypeScript API reference in the [`@a2ui/catalog-mcp` README](https://github.com/a2ui-project/a2ui/blob/main/typescript/catalogs/mcp/README.md) and the [MCP catalog JSON Schema](../../specification/v1.0/schemas/#mcp_catalog.json). +4. Read the startup handshake and JSON-RPC protocol details in the [MCP App component specification](https://github.com/a2ui-project/a2ui/blob/main/catalogs/mcp/v1/mcp_app_specification.md). diff --git a/docs/site/src/styles/custom.css b/docs/site/src/styles/custom.css index 1bb7dbb249..8e7de37166 100644 --- a/docs/site/src/styles/custom.css +++ b/docs/site/src/styles/custom.css @@ -187,3 +187,19 @@ header.header { .a2-callout--danger .a2-callout__title { color: #ef4444; } + +.mermaid-rendered { + margin: 2rem 0; + display: flex; + justify-content: center; + overflow-x: auto; + border-radius: 0.75rem; + padding: 1.25rem; + background: var(--sl-color-bg-inline-code); + border: 1px solid var(--sl-color-hairline); +} + +.mermaid-rendered svg { + max-width: 100%; + height: auto; +}