在 MCP 应用中动态渲染 A2UI 界面:Tools 与 Embedded Resources 集成实战指南
在 MCP 应用中动态渲染 A2UI 界面Tools 与 Embedded Resources 集成实战指南【免费下载链接】a2ui项目地址: https://gitcode.com/GitHub_Trending/a2/a2ui导读本文以 A2UI 开源仓库中的 a2ui-in-mcp-apps 指南 为核心讲解如何在 Model Context ProtocolMCP应用中通过Tools工具与Embedded Resources内嵌资源承载富交互的 A2UI 界面。你将理解「客户端宿主应用 → 双重 iframe 沙箱 → MCP 服务器」的完整通信链路掌握把 Angular/Lit 应用内联为单文件 HTML 资源、通过MessageProcessor渲染 A2UI 负载、以及将用户操作反向回传为 MCP 工具调用的全部工程方法并对照仓库中 a2ui-in-mcpapps 样例 的源码验证每一步实现细节。一、核心思想让 MCP App 直接渲染 A2UI传统集成方式往往由客户端宿主应用负责渲染 UI而本指南提出的模式恰恰相反由沙箱化的 MCP App 直接渲染 A2UI 负载客户端宿主应用只负责转发 MCP 协议消息与提供安全容器。这样带来的收益是MCP 服务器可以与远端 Agent 安全协作无需把渲染能力耦合进宿主所有 A2UI 界面控件、卡片、表单在受限沙箱内统一渲染保持 UI 样式与交互行为的一致性宿主与第三方 MCP App 之间通过结构化 JSON-RPC 消息解耦职责边界清晰。1.1 三个参与角色整个系统由三个主要角色构成一条通信链客户端宿主应用Client Host Application外层容器样例中是一个 Angular 应用负责连接 MCP 服务器并为 MCP App 提供安全的沙箱托管环境MCP 应用沙箱化MCP App运行在双重 iframe 沙箱中的不可信第三方 Web 应用样例中是 Lit 或 Angular 微应用它内部持有 A2UI 渲染表面SurfaceMCP 服务器MCP Server提供应用资源text/html;profilemcp-app并处理工具调用的后端服务。1.2 整体架构图从架构图可以看出服务端一侧由「生成式 A2UI Agent如智能编辑 Agent MCP Server」构成客户端一侧则由「宿主应用」和「双重 iframe 沙箱」构成沙箱内是 MCP App 自己的原生业务逻辑如编辑面板、A2UI Surface如控制面板、App Bridge 与 A2UI 渲染引擎。所有跨可信边界的消息都经由postMessageBridge 中转服务端 Agent 通过 MCP 协议与宿主交换委托负载。二、通信链路深度解析本模式的关键在于MCP App 直接渲染 A2UI 负载而不是依赖客户端宿主应用去渲染。2.1 在 MCP App 中加载 A2UI 组件动态加载 A2UI 组件的完整事件序列如下触发TriggerMCP App 决定需要获取或更新 UI 内容例如初始化时或由用户主动触发某个 Action请求RequestMCP App 通过window.parent.postMessage向宿主发送一条 JSON-RPC 请求样例方法名如ui/fetch_counter_a2ui转发Relay沙箱代理Sandbox Proxy将该消息中继给客户端宿主MCP 调用MCP Call客户端宿主把这条自定义消息翻译成一次标准的 MCPtools/call请求发给 MCP 服务器对应工具如fetch_counter_a2ui响应ResponseMCP 服务器执行工具返回一个包含application/a2uijson资源的结果回传Response forwarding宿主收到工具结果后经沙箱代理下发给 MCP App渲染RenderingMCP App 从资源中提取 A2UI JSON 负载交给本地 A2UIMessageProcessor处理动态更新 A2UI Surface。2.2 处理用户操作反向流程已渲染的 A2UI 表面上的交互通过反转上述流程实现用户在 MCP App 内的 A2UI 表面上点击按钮A2UI 组件触发userActionMCP App 通过 A2UIMessageProcessor.events订阅捕获该事件MCP App 将动作封装为 JSON-RPC 消息发送给宿主如ui/increase_counter宿主调用 MCP 服务器上对应的工具服务器返回新的 A2UI 负载代表更新后的状态再被管道回传给 MCP App 更新渲染。2.3 端到端时序图三、源码级印证样例项目的实现解剖仓库中的 a2ui-in-mcpapps 样例 正是上述架构的完整落地其目录布局为client/宿主容器应用Angular托管外层安全 iframeserver/MCP 服务器Python/uv提供微应用资源与工具server/apps/src/Basic隔离微应用源码server/apps/editor/Editor隔离微应用源码。3.1 服务端资源声明与工具实现MCP 服务器的核心逻辑位于 server.py。它通过app.list_resources()声明两个 MCP App 资源注意其 mimeType 为text/html;profilemcp-appapp.list_resources() async def list_resources() - list[types.Resource]: return [ types.Resource( uriui://basic/app, nameBasic App, mimeTypetext/html;profilemcp-app, descriptionA simple minimal application, ), types.Resource( uriui://editor/app, nameEditor App, mimeTypetext/html;profilemcp-app, descriptionA rich generative document editor, ), ]在read_resource()中服务器按ui://URI 返回对应的单文件 HTMLapp.html或editor.html。源码注释特别强调MCP Apps 要求resources/read的返回内容携带text/html;profilemcp-app的 MIME 类型而不仅是resources/list中声明。工具侧app.list_tools()定义了两类工具工具名类型用途_meta.uiget_basic_app应用入口返回计数器初始负载resourceUri: ui://basic/appvisibility: [model]fetch_counter_a2ui应用工具获取初始计数器 A2UI 负载visibility: [app]increase_counter应用工具计数器 1返回更新值visibility: [app]get_editor_app应用入口打开 Editor A2UI 应用视图resourceUri: ui://editor/appvisibility: [model]smart_editor_get_controls应用工具基于高亮文本生成 A2UI 调优控件visibility: [app]smart_editor_apply应用工具提交用户调好的滑块值经 Gemini 重写文本visibility: [app]这里出现了一个关键机制应用入口工具如get_basic_app通过_meta.ui.resourceUri预声明 UI 模板宿主用resources/read获取 HTML该模板不会作为内嵌资源出现在工具结果里而应用工具如fetch_counter_a2ui则以EmbeddedResource形式在CallToolResult中直接携带application/a2uijson负载。以increase_counter为例它返回的是一个dataModelUpdate消息把计数器新值写回指定表面elif name increase_counter: global COUNTER COUNTER 1 return types.CallToolResult( content[ types.EmbeddedResource( typeresource, resourcetypes.TextResourceContents( uria2ui://ping-result, mimeTypeA2UI_MIME_TYPE, textjson.dumps([{ dataModelUpdate: { surfaceId: ping-result, contents: [ {key: counter, valueNumber: COUNTER} ], } }]), ), ) ] )3.2 A2UI 负载的三段式消息结构Basic App 的完整初始负载存放在 simple_counter_a2ui.json 中它展示了 A2UI 客户端到服务器消息的经典三段式结构dataModelUpdate先写入数据模型例如{key: counter, valueNumber: 0}surfaceUpdate声明组件清单包含Card、Column、Row、Text、Button等组件以及它们通过explicitList表达的子节点顺序控件值与数据模型通过path如{path: /counter}绑定beginRendering指定表面根节点root: root通知渲染引擎开始挂载。按钮动作通过组件上的action字段声明例如{name: increase_counter, context: []}——这个name正是 MCP 服务器上的工具名它构成了「UI 操作 ↔ 工具调用」的映射基础。在 Editor App 中smart_editor_agent.py 更进一步它用 Geminigemini-2.5-flash可通过GENAI_MODEL环境变量覆盖根据用户高亮的文本动态生成 2~3 个slider/select/checkbox调优控件再把控件组装成同样的三段式 A2UI 消息返回。这展示了 A2UI 的生成式能力——服务器侧 Agent 可以直接生成界面本身。3.3 客户端宿主工具白名单与消息转发客户端宿主 app.ts 承担四个职责连接与发现通过SSEClientTransport连接http://127.0.0.1:8000/sse调用client.listTools()发现工具权限收敛从每个工具的_meta.ui.visibility中筛选出visibility包含app的工具组成allowedTools白名单未声明时按 MCP Apps 规范默认[model, app]处理即允许 App 调用模板加载读取入口工具声明的ui://资源获得单文件 HTML 后注入沙箱 iframe请求转发对沙箱内 App 发出的tools/call请求先校验工具名是否在白名单中未通过则返回错误码-32000通过则调用mcpClient.callTool()并把结果回传。同时宿主完成ui/initialize握手应答、ui/notifications/size-changed的 iframe 高度自适应等 MCP Apps 协议细节。所有消息都校验event.origin与event.source确保只与沙箱 iframe 通信。3.4 A2UI 渲染核心MessageProcessor文档引用的 MessageProcessor 是渲染侧的枢纽它暴露processMessages(messages)方法接收ServerToClientMessage[]并把处理结果含userAction通过events: ObservableA2UIClientEvent对外发布。在 Editor App 的 main.ts 中应用通过inject(MessageProcessor)获得处理器用processor.events.subscribe(...)监听用户操作再通过postToParent将tools/call请求上抛给宿主。四、动手实现三步构建带 A2UI 能力的 MCP AppStep 1将渲染器内联为单文件MCP App 通常以单个 HTML 资源的形式由 MCP 服务器下发。若使用 Angular 或 React 等现代框架构建需要把静态资源合并正常构建应用产出静态资源index.html、.js、.css使用后置构建脚本如样例中的 inline.js读取index.html将外链的script src...和link relstylesheet href...替换为内联的script与style标签内容来自真实文件最终得到一个自包含的 HTML 文件可通过受限 iframe 的srcdoc安全加载。从 inline.js 的源码可以看到三个关键细节主入口main.js会用esbuild先做一次--bundle再内联Angular 注入的动态分块link relmodulepreload会被整体清除防止外部资源请求CSS 链接则直接以内联style替换。样例通过node inline.js --input 构建目录 --output 输出文件调用构建脚本见各 app 的package.json。[!TIP]使用 Vite 内联如果你的项目使用 Vite常见于 React、Vue 或 Lit可以用vite-plugin-singlefile在构建阶段自动完成同样的单文件输出无需自定义后置脚本。使用方法安装插件npm install -D vite-plugin-singlefile配置 Vite在vite.config.ts或.js中加入插件import {defineConfig} from vite; import {viteSingleFile} from vite-plugin-singlefile; export default defineConfig({ plugins: [viteSingleFile()], });构建时所有 JS 与 CSS 资源都会被内联进index.html可直接由 MCP 服务器作为单个资源下发。Step 2借助 A2UI-over-MCP 获取并渲染 A2UI内联应用已在沙箱中运行。要发挥 A2UI 能力将A2UI Angular/Lit 库打入应用 bundle与宿主约定通信契约以与 MCP 服务器交互收到宿主响应后在 content 中查找application/a2uijsonmimeType解析 JSON 文本并交给 A2UIMessageProcessor处理。示例获取并渲染 A2UI// 1. Request A2UI data from Host const result await callHostMethod(ui/fetch_counter_a2ui); // 2. Find and parse the A2UI resource const a2uiResource result.find( c c.type resource (c.resource?.mimeType application/a2uijson || c.resource?.mimeType application/jsona2ui), ); if (a2uiResource?.resource?.text) { const messages JSON.parse(a2uiResource.resource.text); this.processor.processMessages(messages); } // Utility for JSON-RPC communication function callHostMethod(method: string, params: any {}): Promiseany { return new Promise((resolve, reject) { const requestId ${method}-${Date.now()}; const handler (event: MessageEvent) { if (event.data.id ! requestId) return; window.removeEventListener(message, handler); if (event.data.error) { reject(event.data.error); } else { resolve(event.data.result); } }; window.addEventListener(message, handler); window.parent.postMessage( { jsonrpc: 2.0, id: requestId, method, params, }, *, ); // Note: Replace * with explicit target origin in production }); }说明processMessages会依次应用负载中的dataModelUpdate、surfaceUpdate与beginRendering消息动态创建并挂载 A2UI Surface如样例中的a2ui-surface surfaceId...自定义元素。Editor App 中还额外调用了processor.clearSurfaces()以在重新生成控件前清空旧表面。Step 3处理 A2UI 组件上的用户操作MCP App 必须捕获 A2UI 事件并将其翻译为 JSON-RPC 消息转发给宿主。示例处理用户操作// Subscribing to A2UI events in the MCP App ([main.ts](https://link.gitcode.com/i/2082d03ccda9e2c3136a6d41abd64bcc)) this.processor.events.subscribe(async event { if (!event.message.userAction) return; const method ui/${event.message.userAction.name}; const params event.message.userAction.context; try { // Translate A2UI UserAction to JSON-RPC, send to Host, and await response const result await callHostMethod(method, params); // Parse the updated A2UI payload and update the rendering const messages extractA2UIMessages(result); if (messages) { this.processor.processMessages(messages); } } catch (error) { console.error(Error handling user action[${method}]:, error); } });这一模式使 MCP App 成为 MCP 服务器 A2UI 能力的动态界面载体同时保持严格的安全隔离。在 Editor App 的实际实现main.ts中还有更细腻的工程细节用户在 A2UI 表面调整滑块后触发smart_editor_apply动作App 会把userAction.context与本地数据模型中的全部控件值合并再发起tools/call服务器返回的可能是 A2UI 负载链式动作也可能是纯文本修订 JSONtext_before/original_text/revised_text/text_afterApp 需要分别处理并在「接受 / 拒绝」按钮上继续驱动本地编辑器的内容更新。内联 MCP App HTML 伪代码以下 HTML 伪代码代表一个编译并内联后的 MCP 应用它定义原生a2ui-surface占位元素初始化AppBridge与外部宿主通信加载时获取动态 A2UI 布局并通过 A2UI SDK 处理事件!DOCTYPE html html langen head meta charsetUTF-8 / titleInlined MCP App Surface/title !-- Assumes the standard A2UI SDK script is bundled or loaded -- /head body div h3MCP App (Editor Panel)/h3 pThis text is native to the sandboxed third-party app./p !-- A2UI Surface custom element provided by the A2UI SDK -- a2ui-surface surfaceIdrecipe-card/a2ui-surface /div script // Note: The pseudocode below assumes AppBridge from modelcontextprotocol/ext-apps // and a2uiProcessor from the A2UI SDK are preloaded or inlined. const bridge new AppBridge({name: editor-panel, version: 1.0.0}); // Helper to extract and process dynamic A2UI responses from tool results function processA2UIResponse(result) { const a2uiResource result?.content?.find( c c.type resource (c.resource?.mimeType application/a2uijson || c.resource?.mimeType application/jsona2ui), ); if (a2uiResource?.resource?.text) { const payload JSON.parse(a2uiResource.resource.text); window.a2uiProcessor.processMessages(payload); } } // 1. Initialize AppBridge and fetch initial controls async function initApp() { await bridge.connect(); // Call server tool to load initial layout controls const result await bridge.callServerTool({name: fetch_controls, arguments: {}}); processA2UIResponse(result); } // 2. Handle interactive User Actions routed by the A2UI SDK window.a2uiProcessor.events.subscribe(async event { if (!event.message.userAction) return; const action event.message.userAction; // Route the user action directly via the bridge to the MCP Server tool const result await bridge.callServerTool({ name: action.name, arguments: action.context, }); // Feed any updated server UI states back to the A2UI processor processA2UIResponse(result); }); // Initialize the app on startup initApp(); /script /body /html五、安全注意事项由于 MCP App 是运行在沙箱中的不可信第三方代码以下两点必须落实显式目标源Explicit Target Origin在已知宿主源的情况下postMessage必须使用具体的targetOrigin如https://trusted-host.com而非*防止恶意 iframe 拦截你的 RPC 请求。样例代码在postToParent中固定使用http://localhost:4200正是这一实践的体现。Null Origin 处理在严格沙箱sandboxallow-scripts且不带allow-same-origin内window.location.origin会被求值为null。因此必须通过比对event.source与期望的窗口对象如window.parent来仔细校验入站消息而不能依赖 origin 字符串判断。宿主侧同样需要校验event.origin与event.source iframe.contentWindow见 app.ts 的消息监听器并对tools/call请求执行工具白名单校验阻断未授权工具调用。六、运行样例动手运行仓库中的 a2ui-in-mcpapps 样例 前请确认环境满足Node.js建议 LTS、uvPython 版本由server/.python-version自动管理并在仓库根目录执行yarn install以链接工作区包。1. 构建客户端沙箱桥在client/目录下执行产物为client/public/sandbox_iframe/sandbox.{js,html}cd client yarn install yarn build:sandbox2. 构建微应用服务器从server/apps/public/下发单文件 HTML这些产物被 git 忽略、不随仓库分发必须先构建至少一个应用服务器本身不构建也能启动。二选一Editor App产物server/apps/public/editor.htmlcd server/apps/editor yarn install yarn build:allBasic App产物server/apps/public/app.htmlcd server/apps/src yarn install yarn build:all3. 启动 MCP 服务器cd server uv sync uv run python server.py --transport sse --port 8000服务器支持stdio与sse两种传输方式--transport参数默认sse。SSE 模式下基于 Starlette uvicorn 提供服务路由为/sse与/messages/样例为便于本地调试对 CORS 使用了allow_origins[*]源码注释明确警告生产环境必须收敛为具体客户端源如http://localhost:4200。4. 启动宿主客户端cd client yarn start浏览器访问http://localhost:4200即可看到运行中的宿主在其中切换 Basic / Editor 应用观察 A2UI 控件在沙箱内动态加载、交互并把状态回传给 MCP 服务器的完整闭环。七、总结A2UI 在 MCP 应用中的集成本质上是把「UI 的生成与渲染」从客户端宿主下沉到沙箱化的第三方 MCP App服务器通过application/a2uijson内嵌资源动态下发界面App 内的MessageProcessor负责解析与渲染用户操作再以userAction→ JSON-RPC →tools/call的路径回到服务器形成完整的生成-渲染-交互-更新闭环。配合ui://模板预声明、单文件 HTML 内联构建与严格的postMessage安全校验这一模式让 MCP 生态中的远程 Agent 能够安全地为用户提供一致、丰富且可交互的动态界面。【免费下载链接】a2ui项目地址: https://gitcode.com/GitHub_Trending/a2/a2ui创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →