在 A2UI 中安全集成 MCP App:基于 mcp-apps-in-a2ui-sample 的双 iframe 沙箱实战解析

发布时间:2026/9/14 22:23:43
在 A2UI 中安全集成 MCP App:基于 mcp-apps-in-a2ui-sample 的双 iframe 沙箱实战解析 在 A2UI 中安全集成 MCP App基于 mcp-apps-in-a2ui-sample 的双 iframe 沙箱实战解析【免费下载链接】a2ui项目地址: https://gitcode.com/GitHub_Trending/a2/a2ui本文基于 a2ui 仓库中的mcp-apps-in-a2ui-sample示例完整讲解如何把第三方 MCPModel Context ProtocolApp 以双重 iframe 沙箱隔离架构嵌入 A2UI 界面并实现「iframe 内 UI 触发动作 → A2UI Client 转发 → Agent 通过 A2A 协议处理 → 返回 surfaceUpdate 更新界面」的双向通信闭环。读完本文你将掌握该示例的架构分层、端到端消息流、McpApp组件与AppBridge桥接机制、sandbox.ts沙箱代理的安全校验逻辑以及开发调试和生产化部署时必须注意的 CORS、CSP 与不可信输入防护要点。示例整体构成Agent Lit Client 的最小可运行闭环该示例位于仓库samples/community/agent/adk/mcp-apps-in-a2ui-sample目录下由两个可独立启动的进程组成Agentagent.py一个基于 FastAPI 的 Python 服务承担两层职责——对外通过/a2a端点响应 A2A 协议消息返回包含McpApp组件的 UI manifest同时处理由 Client 转发过来的工具调用tool call。Clientsamples/community/client/lit/mcp-apps-in-a2ui-sample一个基于 Lit 的浏览器应用渲染 A2UI surface 与McpApp组件并充当 A2A 消息的转发编排器。从 pyproject.toml 可以看到 Agent 侧的依赖画像a2a-sdk0.3.0、google-adk1.28.1、google-genai1.27.0以及fastapi、uvicorn、starlette、python-dotenv。其中python-dotenv对应agent.py开头的load_dotenv()说明 Agent 支持通过环境变量注入密钥等配置a2a-sdk则对应端点返回的 JSON-RPC 2.0 格式的 A2A 任务响应。运行环境要求为 Python 3.10配uv包管理器与 Node.js v18。双 iframe 沙箱隔离模型为什么需要两层运行不受信任的第三方组件代码安全的核心思路是隔离 受限权限。该示例采用双重 iframe 隔离模型三层参与者各司其职Host Page宿主页主 A2UI 应用即 Lit Client持有对整体界面的控制权。Sandbox Proxy沙箱代理托管在独立源127.0.0.1上的 iframe用于强制实现源隔离origin isolation。Untrusted App不受信任的 App真正的 MCP App 内容被动态注入到一个权限受限的内层 iframe 中。宿主与 App 之间的通信由modelcontextprotocol/ext-apps包提供基于标准的postMessage通道完成。这样设计的关键价值在于即便内层 App 代码完全失控它也只能在一个权限被裁剪、来源被隔离的笼子里运行无法直接访问宿主页面的 DOM、存储或网络凭证。这一模型在 sandbox.ts 中有完整实现。沙箱代理在加载时会执行一组自检与校验详见后文并创建内层 iframeconst inner document.createElement(iframe); inner.style.cssText width:100%; height:100%; border:none;; inner.setAttribute(sandbox, allow-scripts allow-forms allow-modals); inner.setAttribute(allow, buildPermissionsPolicy()); document.body.appendChild(inner);注意内层 iframe 的 sandbox 属性只包含allow-scripts、allow-forms、allow-modals并且刻意省略了allow-same-origin——这样浏览器会给该 frame 分配一个匿名唯一源序列化为字符串null使 App 无法以宿主源身份执行特权操作。这也是sandbox.ts中多处使用postMessage(..., *)与允许event.origin null的原因详见下文消息转发小节。三步启动把示例跑起来1. 启动 Client 开发服务器进入客户端示例目录并启动 Vite 服务cd samples/community/client/lit/mcp-apps-in-a2ui-sample yarn dev服务将运行在http://localhost:5173。注意客户端的 vite.config.ts 中server.host被设置为0.0.0.0并配置了fs.allow白名单包括node_modules、shared目录与 renderers/lit 源码这是 Vite 开发服务器允许fs前缀访问工作区文件的前提。2. 启动 Agent另开一个终端进入 Agent 示例目录并启动cd samples/community/agent/adk/mcp-apps-in-a2ui-sample uv run agent.pyAgent 将运行在http://localhost:8000。agent.py末尾的uvicorn.run(app, host0.0.0.0, port8000)将服务绑定到 8000 端口并通过/.well-known/agent-card.json端点对外暴露 A2A 端点地址返回{url: http://localhost:8000/a2a, endpoint: http://localhost:8000/a2a}这符合 A2A 协议中 AgentCard 的发现约定。3. 在浏览器中查看打开http://localhost:5173A2UI 界面会自动加载 MCP App。点击 iframe 内的Call Agent Tool按钮会触发一个由 Agent 处理的动作Agent 读取参数示例中为{foo: bar}后返回surfaceUpdate把 App 替换为一条成功消息Agent processed action: ...。端到端通信链路从加载到工具调用回传客户端示例的 README 将整条链路归纳为 8 步结合源码可以还原出完整的消息流初始加载A2UI Client 加载示例后向 Agent 发送{request: Load MCP App}。对应 mcp-app.ts 中connectedCallback里this.#sendAndProcessMessage({request: Load MCP App})的调用。UI 交付Agent 响应的beginRendering/surfaceUpdate中包含自定义组件McpApp其htmlContent属性携带 App 的原始 HTMLagent.py启动时从 mcp_app.html 读入内存。沙箱隔离Client 用严格的 sandbox 属性allow-scripts allow-forms allow-popups allow-modals allow-same-origin渲染 iframe 隔离 App。握手mcp-apps-component.ts与 iframe 内容通过 postMessage 建立桥接iframe 发送ui/initialize确认就绪。动作触发点击 iframe 内按钮向父窗口A2UI Client发送tools/call消息。动作转发A2UI Client 拦截该消息校验工具名在allowedTools白名单内随后派发a2ui.action事件。Agent 执行框架经 A2A 协议把该动作转发给 Agent。UI 更新Agent 处理后返回surfaceUpdate用成功消息替换原 App 界面。在 Agent 侧agent.py 的/a2a端点用两个分支覆盖上述两段链路当解析到req_text Load MCP App时返回包含McpApp组件的 surface其中三个关键字段是resourceUri: custom://mcp-sample-appApp 的资源标识htmlContent: MCP_APP_HTML从mcp_app.html读取的原始 HTML注入沙箱渲染allowedTools: [trigger_agent_action]允许 App 调用的工具白名单与McpApp组件的allowedTools属性一一对应。当user_action.name trigger_agent_action时打印收到的 contextagent.py使用print(Agent handling trigger_agent_action with context:, context)输出调试信息并返回一个名为mcp-response-surface的新 surface其组件为Text文本为Agent processed action: json.dumps(context)从而实现点击按钮 → 回显处理结果的完整闭环。McpApp 组件A2UI 与 MCP App 之间的桥接器核心桥接逻辑位于 mcp-apps-component.ts 的McpApp类中。它通过property声明了与服务端契约一致的属性resourceUristring、htmlContentstring、heightnumber|undefined缺省时按 4:3 宽高比渲染、allowedToolsstring[]。组件在收到htmlContent且 iframe 就绪后调用initializeSandbox()核心步骤为确定沙箱代理 URL优先读取VITE_MCP_SANDBOX_URL环境变量用于生产部署否则回退到本地开发用的http://127.0.0.1:${window.location.port}${SANDBOX_IFRAME_PATH}——利用127.0.0.1与localhost在浏览器中被视为不同源的特点模拟跨源隔离。实例化 AppBridgenew AppBridge(null, {...}, {serverTools: {}, updateModelContext: {text: {}}}, {hostContext: {theme: light, platform: web, displayMode: inline}})。注释明确说明不需要 MCP client因为 A2UI 自身充当编排器。监听尺寸变化bridge.onsizechange允许 App 动态调整 iframe 容器尺寸带 300ms 的 ease-out 过渡动画。拦截工具调用bridge.oncalltool中校验params.name是否命中allowedTools白名单——命中则调用dispatchAgentAction并把工具调用转换为 A2UI 的a2ui.action事件通过dispatchEvent(new v0_8.Events.StateEvent(eventPayload))派发否则console.warn并抛出Error(Tool not allowed)拒绝执行。等待代理就绪监听ui/notifications/sandbox-proxy-ready通知McpUiSandboxProxyReadyNotification确认沙箱代理 iframe 加载完毕。连接桥bridge.connect(new PostMessageTransport(this.iframe.contentWindow!, this.iframe.contentWindow!))——只向沙箱代理窗口发送消息。下发 UI 资源bridge.sendSandboxResourceReady({html: this.htmlContent, sandbox: allow-scripts allow-forms allow-popups allow-modals allow-same-origin})把内层 App 的 HTML 与 sandbox 属性交给代理去注入内层 iframe。在dispatchAgentAction中工具参数会被扁平化为 A2UI Action 的 context 数组字符串映射为literalString、数字为literalNumber、布尔为literalBoolean、对象为JSON.stringify后的literalString——这与a2ui.action事件的标准数据结构保持一致。沙箱代理 sandbox.ts源码级安全机制拆解sandbox.ts 是双 iframe 架构中代理一层的完整实现其安全设计可以从五个层面理解加载时自检文件在顶层窗口window.self window.top直接抛错This file is only to be used in an iframe sandbox.无document.referrer或 referrer 不匹配ALLOWED_REFERRER_PATTERN时同样抛错。默认的 referrer 模式为/^http:\/\/(localhost|127\.0\.0\.1)(:|\/|$)/可通过VITE_ALLOWED_HOST_ORIGIN环境变量覆盖为生产域名正则做了转义处理。安全自测self-test除非 URL 带disable_security_self_testtrue参数否则沙箱会尝试window.top!.alert(...)——如果能在顶层弹出 alert说明沙箱隔离失效直接抛错。这是对iframe 是否真的被隔离的运行时探测。敏感权限默认禁用SENSITIVE_PERMISSIONS列出camera、microphone、geolocation、clipboard-read、clipboard-write五项buildPermissionsPolicy会为未显式授予的敏感特性追加none指令最终拼成内层 iframe 的allow属性。消息来源双重校验来自父窗口event.source window.parent的消息其 origin 必须与从 referrer 推导出的期望宿主源一致normalizeOrigin会把127.0.0.1归一化为localhost再比较来自内层窗口event.source inner.contentWindow的消息其 origin 必须等于沙箱自身源或字符串null因为内层 iframe 未开allow-same-origin浏览器强制其为匿名唯一源。消息转发与通知收到sandbox-resource-ready类通知后把html/htmlContent用srcdoc注入或url用src加载写进内层 iframe并在加载完成后向 App 发送{type: sandbox-init}。其余消息在父子窗口间透明转发。最后沙箱代理会同时向宿主发送两种就绪通知标准的 JSON-RPC 2.0 格式ui/notifications/sandbox-proxy-ready供标准 MCP Apps 组件使用和 A2UI 扁平信封格式a2ui_sandbox_proxy_ready供 A2UI WebAppFrame 组件使用且注释说明两种通知对另一方均为可安全忽略的 no-op——这使得同一套沙箱代理可以兼容两种宿主组件。此外注释特别强调内层 iframe严格省略allow-top-navigation/allow-top-navigation-by-user-activation防止内嵌脚本劫持顶层导航的 frame-busting以及allow-popups/allow-popups-to-escape-sandbox防止一键点击式链接外泄。开发环境注意事项模块解析、CORS 与 CSP原文档的 Development Notes 是本地调试最容易踩坑的三个点这里结合 vite.config.ts 展开模块解析Module Resolution开发环境下iframe 通过 Vite 的/fs/前缀从工作区node_modules动态加载app-with-deps.js打包产物vite.config.ts中的serve-sandbox插件把/sandbox/...路径SANDBOX_BASE_PATH改写为fs绝对路径并把.js回写为.tsapp-bridge.js、app-with-deps.js除外同时把/lit/node_modules/直接映射到../node_modules下的真实文件。mcp_app.html内部则用script typeimportmap把modelcontextprotocol/sdk的裸导入解析到对应 ESM 文件保证沙箱内的 bundle 能正常加载依赖。CORSiframe 必须从127.0.0.1加载以匹配沙箱代理期望的源并避免 CORS 拦截。这正是mcp-apps-component.ts中sandboxOrigin默认值取http://127.0.0.1:${window.location.port}${SANDBOX_IFRAME_PATH}的原因。CSPContent Security Policysandbox.html使用静态 CSP允许unsafe-inline和unsafe-eval以兼容开发工具生产部署必须移除这些宽松设置并按 MCP Apps 规范实现由 App 元数据动态推导的 CSP。此外VITE_MCP_SANDBOX_URL沙箱代理地址与VITE_ALLOWED_HOST_ORIGIN宿主源白名单这两个环境变量即为生产化改造的预留扩展点。安全边界与生产化建议把外部 Agent 当不可信实体原文档的 Disclaimer 部分是本示例最重要的工程结论值得在实现层面展开A2A 协议数据一律视为不可信输入任何在直接控制之外运行的 Agent 都应被当作潜在恶意实体。其 AgentCard、messages、artifacts、任务状态等运营数据若未经清洗直接拼入 LLM 提示词可能引发prompt injection例如恶意 Agent 在name、skills.description字段中构造对抗数据。UI 定义与数据流同样不可信恶意 Agent 可能伪造合法界面诱骗用户phishing、通过属性值注入恶意脚本XSS、或生成过度复杂的布局拖垮客户端性能DoS。内嵌内容需额外防护如果应用支持 iframe / web view 之类的可选内嵌内容必须防止跳转到恶意外部站点。示例本身的双 iframe 沙箱 referrer/origin 校验 敏感权限默认禁用正是这一要求的落地示范。开发者责任不充分校验数据、不严格沙箱化渲染内容会引入严重漏洞。生产实现必须落实输入清洗input sanitization、内容安全策略CSP、对内嵌内容的严格隔离、安全凭证管理secure credential handling。与仓库内其他示例的关系该示例在实现模式上参考了仓库中custom-components-example尤其是 floor plan map 的通信方式iframe 内使用原生 postMessage而非从unpkg.com这类外部 CDN 加载AppBridge脚本从而对网络抖动和沙箱内 CSP 限制更加健壮同时把编排职责交给 A2UI 宿主由它把 MCP 工具调用翻译为 A2UI action。这一宿主即编排器的模式也让 sandbox.ts 这份共享实现能够同时服务McpApp组件与 WebAppFrame 两种宿主形态是整个 MCP-in-A2UI 方案里可复用性最强的部分。【免费下载链接】a2ui项目地址: https://gitcode.com/GitHub_Trending/a2/a2ui创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询