A2UI Angular 客户端实战:通过 MCP Apps、Iframe URL 与 Srcdoc 三种方式嵌入外部 Web 应用
【免费下载链接】a2ui项目地址: https://gitcode.com/GitHub_Trending/a2/a2ui
本文基于samples/community/client/angular/projects/mcp_calculator示例,讲解如何在 A2UI Angular 客户端中加载并展示一个由 Agent 驱动的交互式 Pong 游戏界面。该示例通过同一套 A2UI 协议演示了三种截然不同的 Web 应用嵌入路径——MCP Apps(沙箱内嵌)、Iframe by URL(远程地址直载)与 Iframe by Srcdoc(内联 HTML 注入),读者读完可以掌握完整的运行配置、三种嵌入方式的原理与差异、以及沙箱隔离与消息安全的核心实现要点,并能够直接复刻到自己的 A2UI 客户端项目中。
示例概览:一个客户端、三种召唤方式
mcp_calculator是一个 Angular 单页应用,它通过 A2A 协议连接后端的 MCP Apps Proxy Agent,由 Agent 根据用户指令返回对应的 A2UI 布局 payload,客户端据此渲染出 Pong 游戏界面与计分板。示例的独特之处在于,它把"如何把一个 Web 游戏嵌入到客户端"这一问题的三种解法放在同一个界面里对比:
| 召唤方式 | 对应 A2UI 组件 | 内容来源 | 关键特征 |
|---|---|---|---|
| MCP Apps | McpApp | Agent 将 HTML 与 JS 逻辑直接打包进组件 payload | 沙箱 iframe + JSON-RPC over window messaging |
| Iframe by URL | WebAppFrameUrl | 远程 Pong Web 服务器的 URL(http://localhost:8081/pong_app_web_frame.html) | 浏览器直接从 HTTP 端点加载游戏帧 |
| Iframe by Srcdoc | WebAppFrameSrcdoc | Agent 在后端抓取远程 HTML 后内联传输 | 使用 iframe 的srcdoc属性渲染 HTML 字符串 |
这三种方式共享同一个游戏逻辑(Pong),但内容投递与隔离策略完全不同,非常适合作为理解 A2UI 嵌入式内容渲染机制的对照实验。
环境准备与依赖
运行本示例需要满足以下前置条件:
- Node.js 18 或更高版本,以及Yarn包管理器;
- Python 3.9 或更高版本,并安装
uv; - Pong Web 服务器:托管游戏端点,运行说明见 samples/community/web/pong/README.md;
- MCP Apps Proxy Agent 后端:运行说明见 samples/community/agent/adk/mcp_app_proxy/README.md。
其中 Agent 后端基于 Agent Development Kit(ADK)与 A2A 协议构建,本身是一个 A2A 服务器,需要配置 LLM 的 API Key(复制.env.example为.env后填写)。
完整运行步骤
按照以下顺序依次启动即可跑通整个示例:
第 1 步:构建仓库根目录的共享工作区依赖
yarn build:all该命令会构建 A2UI 的共享 workspace 依赖(如@a2ui/angular、@a2ui/web_core等),供后续的示例项目引用。
第 2 步:安装samples/community目录下的本地依赖
cd samples/community yarn install第 3 步:启动 Pong Web 服务器
cd samples/community/web/pong uv run .服务器会监听8081端口,控制台输出Serving at port 8081。它实际是一个 Python HTTP 静态服务器:请求/pong_app_web_frame.html时,动态读取共享的pong_base.html与pong_engine.js(位于samples/community/agent/adk/mcp_app_proxy/目录),注入本地桥接脚本pong_web_frame_bridge.js后组装成完整页面,并附带相应的 CORS 头。它还额外提供了/pong_app_web_frame_srcdoc.html端点,供WebAppFrameSrcdoc场景由 Agent 在后端远程抓取。
第 4 步:在另一个终端启动 Agent 后端
cd samples/community/agent/adk/mcp_app_proxy uv run .第 5 步:启动 Angular 应用
cd samples/community/client/angular yarn start mcp_calculator第 6 步:打开浏览器访问
http://localhost:4200/?disable_security_self_test=trueURL 中的disable_security_self_test查询参数用于关闭沙箱 iframe 的原生安全自测(origin toggle 相关检查),便于在本地开发环境中直接运行。
界面交互:三个建议按钮触发三种渲染路径
应用加载后,主界面(基于A2aChatCanvas构建,Agent 名称显示为 "MCP Calculator",见 app.ts)会展示三个建议按钮:
- Open Pong as MCP App
- Open Pong from remote web server
- Open Pong with WebApp Srcdoc
点击任意按钮,客户端即向后端 Agent 发送一条消息(通过ChatService.sendMessage),Agent 返回对应的 A2UI 布局 payload,界面随即更新为游戏区域 + 计分板 + 解说组件。计分板是 A2UI 原生组件(PongScoreBoard),通过playerScore、cpuScore、commentary三个动态属性实时展示比分与解说;而游戏区域则由三种不同的嵌入组件分别承载。整个布局由 pong-layout.ts 中的PongLayout组件组合而成。
方式一:MCP Apps 沙箱内嵌
这是三种方式中能力最完整的一种。Agent 将游戏的全部 HTML 与 JavaScript 逻辑直接写入McpApp组件的htmlContent属性,客户端在沙箱 iframe 中渲染,宿主与内嵌应用之间通过 window messaging 上的 JSON-RPC 协议通信。
组件属性契约
从 catalog.ts 中McpAppSchema的定义可以看到McpApp支持的属性:
| 属性 | 类型 | 说明 |
|---|---|---|
htmlContent | 动态字符串 | 游戏 HTML 内容,支持url_encoded:前缀的 URL 编码形式 |
allowedTools | 字符串数组 | 允许内嵌应用调用的工具白名单 |
allowedFunctions | 字符串数组 | 允许内嵌应用调用的本地函数白名单 |
data | 动态值 | 数据绑定路径映射,实现与宿主 Data Model 的双向同步 |
title | 动态字符串 | iframe 的标题 |
渲染链路与桥接机制
McpApp 组件的渲染流程可以概括为:
- 内容解析:读取
htmlContent,若以url_encoded:开头则先decodeURIComponent解码; - 加载沙箱宿主:iframe 首先加载同源的
mcp_apps_inner_iframe/sandbox.html(不直接加载游戏内容),从而获得宿主页面的window.location.origin; - 桥接握手:沙箱页面加载完成后通过
SANDBOX_PROXY_READY_METHOD消息通知宿主,宿主收到后通过AppBridge+PostMessageTransport建立 JSON-RPC 连接,并把解析出的 HTML 通过sendSandboxResourceReady注入沙箱,sandbox属性为allow-scripts; - 双向数据绑定:
- 宿主侧通过
surface.dataModel.subscribe(dataPath, ...)订阅 Data Model 变化,把增量变化以ui/notifications/data-model-update通知推送给应用;对对象类型还会逐键 diff,避免子路径相互覆盖; - 应用侧通过
ui/notifications/data-model-change通知写回宿主,写入前会经过validateMessageSecurity安全校验,并用isProcessingAppWrite标志抑制回显,防止反馈循环;
- 宿主侧通过
- 函数与工具调用:
- 应用发起
ui/requests/function-call时,先校验参数安全性,再检查函数名是否在allowedFunctions白名单内,最终通过surface.catalog.invoker在客户端本地执行; - 应用发起工具调用
oncalltool时,先校验参数与allowedTools白名单,再通过surface.dispatchAction以 Action 形式派发给宿主处理;
- 应用发起
- 动态尺寸同步:应用可通过
onsizechange回调请求调整尺寸,宿主侧实现了一整套节流策略——宽度钳制在 200–3000px、高度钳制在 100–2000px,变化超过 5px 阈值才生效,且用 100ms 定时器节流,避免频繁重排;同时用ResizeObserver把宿主容器尺寸反向同步给应用(setHostContext)。
组件销毁时还会依次清理数据订阅、消息监听、ResizeObserver并关闭AppBridge,避免内存泄漏与僵尸事件监听。
方式二:Iframe by URL 远程直载
WebAppFrameUrl组件的载荷只有一个 URL,指向远程 Pong 服务器(http://localhost:8081/pong_app_web_frame.html),由浏览器直接加载该页面。它的核心价值在于:游戏逻辑完全托管在远端,客户端只负责提供一个安全的外壳。
从 web-app-frame-url.ts 的实现可以看到几个关键的安全设计:
- 协议白名单:
targetUrl计算时先通过new URL()解析,仅允许http:与https:协议,从根源上阻止javascript:、data:、file:等协议的注入; - origin 传递:URL 上会附加
origin查询参数(当前客户端窗口的window.location.origin),供远端页面识别宿主来源; - 期望源校验:
expectedOrigin从 URL 推导出目标页面的 origin,沙箱握手时据此校验消息来源; - 宿主外壳复用:iframe 首先加载同源的
sandbox-url.html沙箱宿主,待SandboxResourceReady握手后,再把带 origin 参数的目标 URL 通过 postMessage 交给沙箱去加载,从而保证远程内容在受控的沙箱中渲染。
方式三:Iframe by Srcdoc 内联注入
WebAppFrameSrcdoc与方式二的差别在于内容投递路径:Agent 在后端主动抓取远程 Web 服务器的游戏 HTML(pong_app_web_frame_srcdoc.html),把原始 HTML 字符串内联到htmlContent属性中随 A2UI payload 一起传输,客户端再把该字符串塞进 iframe 的srcdoc属性渲染。
由于 HTML 内容完全来自(可能不可信的)Agent,web-app-frame-srcdoc.ts 在注入前做了两道强化防护:
- 强制注入 Content-Security-Policy(CSP):任何作者自带的 CSP meta 标签都会被剥离,并注入如下受限默认策略:
default-src 'self' 'unsafe-inline' 'unsafe-eval' data:; connect-src 'none'; form-action 'none'; base-uri 'none'; object-src 'none'; frame-src 'none';该策略的效果是:connect-src 'none'阻断fetch、XMLHttpRequest、WebSocket、EventSource等一切外发网络连接;form-action 'none'封堵表单提交外泄路径;base-uri 'none'阻止基址劫持;object-src与frame-src禁止插件对象与嵌套子帧。
- 注入链接点击拦截器:页面内所有
<a>链接点击都会被拦截,除锚点与javascript:外一律preventDefault,并把目标 URL 以a2ui_action/open_url消息转发给宿主,由宿主决定如何处理,而不是让沙箱内页面直接跳转。
同时sandbox属性被设定为allow-scripts allow-forms allow-modals——刻意省略allow-same-origin(保持源隔离)、allow-top-navigation(防框架逃逸)与allow-popups(防一键链接外泄),且消息发送目标固定为window.location.origin,保证消息只在同源宿主间传递。
三种方式的取舍与选型建议
综合源码实现,三种方式形成了清晰的取舍光谱:
- MCP Apps(
McpApp):能力最完整——支持双向数据绑定、本地函数调用、工具调用白名单与动态尺寸协商,适合需要与宿主深度交互、共享 A2UI Data Model 的嵌入式应用;代价是需要在客户端实现完整的AppBridge桥接逻辑,且所有交互消息都要经过安全校验与白名单过滤(validateMessageSecurity在数据写回、函数参数、工具参数三个入口都会执行,见 web-frame-messages.ts)。 - Iframe by URL(
WebAppFrameUrl):实现最轻、隔离性最好,内容托管在远端可独立演进;适合内容更新频繁、不需要与宿主数据模型深交互的场景,代价是依赖远端服务器可用性与 CORS 配置。 - Iframe by Srcdoc(
WebAppFrameSrcdoc):把"从 URL 加载"变成"内容随载荷走",一次握手即可注入,且通过强制 CSP 与链接拦截提供了最严格的静态内容防护;适合内容由 Agent 聚合、但无需与宿主双向通信的场景。
扩展实验:自定义目录组件与本地函数
mcp_calculator还展示了如何为客户端定义一套完整的自定义目录。catalog.ts 中的DEMO_CATALOG注册了六个组件(McpApp、PongScoreBoard、PongLayout、WebAppFrameUrl、WebAppFrameSrcdoc、Column),其 Catalog ID 与 Agent 侧的mcp_app_catalog.json保持一致(https://a2ui.org/samples/community/agent/adk/mcp_app_proxy/catalogs/0.9/mcp_app_catalog.json),保证 Agent 返回的 payload 能被客户端正确解析。
此外,示例还通过createFunctionImplementation注册了showWinnerModal本地函数:比赛结束时内嵌应用调用该函数,客户端弹出胜者对话框,"Play Again" 按钮通过context.surface.dataModel.set('/pong_state/player_score', 0)等调用直接重置 A2UI Data Model 中的比分——这正是"本地函数执行"与"数据模型回写"两个机制的直观演示。
安全提醒
A2UI 的嵌入式内容机制(iframes、web views)会引入不可信内容,参照 mcp_app_proxy README 的免责声明,Agent 返回的任何 UI 定义与数据流都应视为不可信输入:恶意 Agent 可能伪造界面诱导用户(钓鱼)、通过属性值注入恶意脚本(XSS)或构造过重的布局拖垮客户端(DoS)。本文示例中的协议白名单、期望源校验、CSP 注入、链接拦截与allowedTools/allowedFunctions白名单机制,正是生产环境必备的加固手段;在实际项目中,还应对 AgentCard、消息与任务状态等全部外部数据做同样严格的清洗与隔离。
【免费下载链接】a2ui项目地址: https://gitcode.com/GitHub_Trending/a2/a2ui
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考