A2UI in MCP Apps:Angular 宿主容器的双 iframe 安全隔离架构与实现解析
2026/9/14 21:31:29 网站建设 项目流程

A2UI in MCP Apps:Angular 宿主容器的双 iframe 安全隔离架构与实现解析

【免费下载链接】a2ui项目地址: https://gitcode.com/GitHub_Trending/a2/a2ui

本文围绕 A2UI 项目中a2ui-in-mcpapps示例的 MCP Host 应用组件展开,深入解析其如何以双 iframe 代理模式安全托管由 MCP 服务器提供的不受信任第三方 Angular 微应用(MCP Apps),实现工具调用转发、布局消息透传与 UI 无关的隔离沙箱基础设施。读完本文,你将掌握该宿主的完整通信协议(ui/initializetools/call中继、ui://资源加载)、工具可见性白名单机制,以及从构建沙箱桥接到启动服务端与客户端的完整实操流程。

一、关联文档与示例定位

本篇文章的核心文档位于 samples/community/mcp/a2ui-in-mcpapps/client/src/app/README.md,它描述的是 MCP Host Application 的核心应用组件。该示例所在的完整目录为 samples/community/mcp/a2ui-in-mcpapps,整体架构由三部分构成:

  • Client(宿主应用):Angular 编写的宿主容器,持有外层安全 iframe,是本文的主角;
  • Server(MCP 服务器):基于 Python/uv 的 MCP 服务器,提供工具(tools)与微应用资源(resources),实现位于 server.py;
  • Isolated Micro-Apps(隔离微应用):由服务器提供、在宿主内渲染的不受信任组件,包括 Basic 计数器应用与 Editor 生成式编辑器应用。

该宿主应用的核心设计原则是:对 MCP 服务器下发的不受信任第三方组件进行安全隔离与托管,同时自身不持有任何 A2UI 或应用特定的渲染知识——它只管理隔离沙箱所需的基础设施框架,实现"UI 无关的隔离基础设施"(UI-Agnostic Isolation Infrastructure)。这意味着宿主可以托管任何符合 MCP Apps 规范的微应用,而无需了解其内部 UI 逻辑。

二、核心特性:四层职责拆解

根据文档,该宿主应用具备四项关键能力:

特性说明
安全隔离(Security Isolation)实现安全的双 iframe 代理模式(double-iframe proxy pattern),对不受信任组件进行隔离测试,在维持功能的同时防止安全泄露
宿主容器(Host Container)作为外层安全 iframe 的主容器
工具与消息透传(Tool & Message Passthrough)作为通信桥梁,在隔离 iframe 与数据库/MCP 服务器之间路由自定义工具调用与布局消息
UI 无关隔离基础设施不持有任何 A2UI 或应用特定渲染知识,仅管理隔离沙箱所需的基础设施框架

其中"工具与消息透传"提到的"数据库/MCP 服务器",在源码层面体现为宿主通过@modelcontextprotocol/sdkClient与 Python MCP 服务器建立 SSE 连接,并将微应用发出的tools/call请求转发给服务器,服务器返回的 A2UI JSON 载荷再回传至微应用进行渲染。

三、双 iframe 代理模式:源码级深入

3.1 沙箱代理的初始化与安全自检

双 iframe 模式的中间层——沙箱代理——位于 samples/client/shared/mcp_apps_inner_iframe/sandbox.ts。它通过yarn build:sandbox构建为独立 bundle,输出到宿主客户端的public/sandbox_iframe/sandbox.{js,html},随后由宿主在connectAndLoadApp()中设置 iframe 的src指向该沙箱页面:

this.appIframe.nativeElement.src = '/sandbox_iframe/sandbox.html?disable_security_self_test=true';

沙箱代理加载后依次执行以下安全检查:

  1. iframe 使用校验window.self === window.top时直接抛错,确保该文件只在 iframe 沙箱内运行;
  2. referrer 校验:无document.referrer则拒绝加载;默认只允许http://localhost127.0.0.1开头的嵌入来源,生产环境可通过VITE_ALLOWED_HOST_ORIGIN环境变量配置允许的宿主来源;
  3. 安全自检(security self-test):默认调用window.top.alert(...),若能成功弹出说明隔离失效,直接抛错拒绝运行;仅在 URL 带disable_security_self_test=true时跳过(供测试使用)。

3.2 内层 iframe 的沙箱属性

代理随后创建一个内层 iframe 承载不受信任的 HTML 内容,并刻意省略allow-top-navigationallow-top-navigation-by-user-activation,防止嵌入脚本劫持顶层窗口导航(frame-busting 防护):

const inner = document.createElement('iframe'); inner.style.cssText = 'width:100%; height:100%; border:none;'; inner.setAttribute('sandbox', 'allow-scripts allow-forms allow-popups allow-modals'); document.body.appendChild(inner);

结合外层宿主 iframe,形成三层结构:Angular 宿主页面 → 沙箱代理 iframe(独立安全边界)→ 内层 iframe(运行不受信任微应用)。宿主只与代理通信,代理与微应用通信,微应用永远无法直接触达顶层窗口。

3.3 资源投递握手

代理与宿主之间通过两条专用通知完成 HTML 资源投递:

  • 代理就绪后向宿主发送ui/notifications/sandbox-proxy-ready
  • 宿主收到后回发ui/notifications/sandbox-resource-ready,并在params.html中携带微应用的完整 HTML 内容。

宿主侧对应处理逻辑位于 app.ts 的ngAfterViewInit()消息监听中。这一设计保证了:只有代理确认就绪后,宿主才会把不受信任的 HTML 注入沙箱,避免注入时机竞态带来的安全风险。

四、宿主容器的消息协议与中继实现

宿主在ngAfterViewInit()中注册统一的window.addEventListener('message', ...),并执行两道来源校验event.origin必须等于window.location.origin,且event.source必须是appIframecontentWindow。只有通过校验的消息才会进入协议分发,防止第三方页面伪造消息。宿主支持的消息方法如下:

方法方向宿主行为
ui/notifications/sandbox-proxy-ready代理 → 宿主回发ui/notifications/sandbox-resource-ready,携带html内容
ping代理 → 宿主原样回包{jsonrpc:'2.0', id, result:{}},用于探测连通性
ui/initialize代理 → 宿主返回protocolVersion: '2026-01-26'hostInfohostCapabilitieshostContextdisplayMode: 'inline'
ui/notifications/initialized代理 → 宿主微应用已初始化,宿主随后投递ui/notifications/tool-input(实例化工具调用的入参)与ui/notifications/tool-result(调用结果)
ui/notifications/size-changed代理 → 宿主根据params.height动态调整 iframe 高度,实现微应用自适布局
tools/call代理 → 宿主校验工具名是否在白名单内,通过后经 MCP Client 转发至服务器,结果回传代理

4.1 布局消息透传

微应用通过ui/notifications/size-changed通知宿主其内容高度,宿主据此实时调整 iframe 的style.height。这正是文档所说"路由布局消息"的具体落地,让宿主无需理解微应用内部 UI 即可完成自适应布局。

4.2 工具调用中继与白名单拦截

当微应用发起tools/call时,宿主先检查this.allowedTools.has(toolName)

  • 不在白名单内:打印[Host] Blocked unauthorized tool call: ...,并以 JSON-RPC 错误码-32000返回Tool '...' is not whitelisted.拒绝执行
  • 在白名单内:调用this.mcpClient.callTool({name, arguments})转发至服务器,成功与失败均以 JSON-RPC 格式回传代理。

该白名单的构建逻辑在下节详述。从实现可见,宿主在安全上采取默认拒绝策略:只放行显式声明可被应用调用(visibility包含"app")的工具。

五、工具可见性白名单:_meta.ui.visibilityui://资源

5.1 白名单构建规则

宿主的connectAndLoadApp()在连接 MCP 服务器后先调用client.listTools(),遍历所有工具并按_meta.ui.visibility构建allowedTools

this.allowedTools = new Set( tools .filter(tool => { const visibility = (tool._meta as any)?.ui?.visibility; return Array.isArray(visibility) ? visibility.includes('app') : APP_CALLABLE_WHEN_VISIBILITY_UNDECLARED; }) .map(tool => tool.name), );

规则如下:

  • 显式声明visibility为数组且包含'app'时放行(模型与应用均可调用);
  • 未声明:按 MCP Apps 规范默认视为["model", "app"],即默认可被应用调用。源码中通过常量APP_CALLABLE_WHEN_VISIBILITY_UNDECLARED = true显式表达这一宽松默认值,并注释说明"更严格的宿主可以选择拒绝未显式声明 app 可见性的工具"。

5.2 服务器端的工具声明

对应地,服务器在 server.py 中通过_meta.ui声明两类工具:

  • 入口工具(仅模型可调,visibility: ["model"],声明resourceUri):get_basic_app指向ui://basic/appget_editor_app指向ui://editor/app
  • 应用可调工具visibility: ["app"]):fetch_counter_a2uiincrease_countersmart_editor_get_controlssmart_editor_apply

入口工具通过_meta.ui.resourceUri预先声明 UI 模板ui://协议),宿主据此调用resources/read拉取微应用 HTML,而不会在工具结果中内嵌资源——这与旧式的"结果内嵌 UI"方案形成关键差异。

5.3ui://资源加载链路

宿主加载微应用的完整链路为:

  1. client.listTools()发现入口工具及其_meta.ui.resourceUri
  2. 校验resourceUriui://开头,否则抛错;
  3. client.callTool({name: entryTool})触发工具,结果暂存为toolCallResult
  4. client.readResource({uri: resourceUri})读取微应用 HTML;
  5. 校验资源mimeType === 'text/html;profile=mcp-app'或包含text字段,将text存入this.htmlContent
  6. 设置 iframesrc指向沙箱页面,待sandbox-proxy-ready后投递 HTML。

服务器端read_resource严格按 MCP Apps 要求,在resources/read返回的内容上携带text/html;profile=mcp-appMIME 类型(而不仅是resources/list中声明),并映射ui://basic/appapps/public/app.htmlui://editor/appapps/public/editor.html

5.4 工具结果与输入投递时序

微应用完成ui/initialize后发送ui/notifications/initialized,宿主在此之前不得向微应用发送消息;收到该通知后,宿主按序投递:

  1. ui/notifications/tool-inputparams.arguments)——实例化该 View 的工具调用入参;
  2. ui/notifications/tool-resultparams)——调用结果,其中携带application/a2ui+jsonMIME 类型的嵌入式资源(如simple_counter_a2ui.json中的初始计数器载荷、dataModelUpdate增量更新)。

例如点击计数器按钮后,微应用经代理向宿主发送tools/callincrease_counter),服务器返回dataModelUpdate载荷更新counter数值,再沿链路回传微应用并刷新 A2UI 渲染。

六、端到端通信时序

以下序列图完整描述了从宿主加载到 A2UI 组件交互的十一条消息链路(源自示例根文档 README.md 的架构图):

该链路揭示了两个关键事实:

  • 微应用自身负责 A2UI 渲染:步骤 7~9 说明 A2UI Surface 由微应用内部渲染,宿主不参与;A2UI 按钮触发UserAction后由微应用映射为tools/call请求;
  • 宿主只做中继:无论工具调用还是 A2UI 载荷,宿主仅校验、转发、回传,不解析内容——这正是"UI 无关隔离基础设施"的直接体现。

七、宿主 UI 与交互模板

宿主模板 app.html 提供极简交互界面:

  • 状态栏:Status: {{ status() }},反映Not connectedConnecting to MCP Server...Initializing MCP Client...Listing tools...Calling MCP App tool...Reading resource: ...App loaded successfully!Error: ...等完整生命周期;
  • 应用选择器:editor(Generative Editor App)与basic(Basic Counter App)两个选项,通过onAppChange()校验并更新selectedApp
  • 加载按钮:触发connectAndLoadApp(),加载期间禁用选择器与按钮;
  • 内容区:宽度 100%、高度 600px 的 iframe(#appIframe),由宿主在加载完成后设置其src指向沙箱页面。

其中onAppChange只接受'editor' | 'basic'两个枚举值,其他输入记录错误并忽略,防止非法应用注入。测试用例 app.spec.ts 验证了组件可创建且渲染Simple MCP Apps Host标题。

八、构建与运行:从零到完整链路

8.1 前置依赖

  • Node.js(推荐 LTS);
  • uv(Python 版本由server/.python-version自动管理);
  • 仓库根目录执行yarn install链接 workspace 包。

客户端依赖栈(见 client/package.json)包括@angular/core21.x、@modelcontextprotocol/sdk^1.29.0、@modelcontextprotocol/ext-apps^1.7.4 等。

8.2 第一步:构建客户端沙箱桥接

client/目录执行:

cd client yarn install yarn build:sandbox

该命令通过esbuild将 sandbox.ts 打包为client/public/sandbox_iframe/sandbox.js,并复制sandbox.html到同目录——这是沙箱代理的运行时资产,宿主页面依赖此产物才能加载

8.3 第二步:构建微应用

服务器从server/apps/public/目录伺服单文件 HTML 产物。这些产物被 git 忽略、不随仓库分发,因此必须至少构建一个应用后才能加载其界面(服务器本身即使没有产物也能启动)。两种选择:

选项 A:Editor 应用

cd server/apps/editor yarn install yarn build:all

生成server/apps/public/editor.html

选项 B:Basic 应用

cd server/apps/src yarn install yarn build:all

生成server/apps/public/app.html

构建流程(详见 server/apps/README.md):先由 Angular 编译原始产物到dist/raw,再由inline.js将全部 JavaScript 与 CSS动态内联进index.html,输出单一自包含文件。这是 MCP App 安全隔离要求的必然结果——沙箱 iframe 内的应用必须自包含,不能依赖额外的网络资源加载。

8.4 第三步:启动 MCP 服务器

server/目录执行:

cd server uv sync uv run python server.py --transport sse --port 8000

服务器参数:--transport可选stdiosse(默认sse),--port默认8000。SSE 模式下基于 Starlette 暴露/sse端点与/messages/消息挂载点,并启用 CORS(示例中allow_origins=["*"],源码注释明确警告生产环境必须限制为客户端来源,例如http://localhost:4200)。

8.5 第四步:启动宿主客户端

client/目录执行:

cd client yarn start

浏览器访问http://localhost:4200。宿主通过SSEClientTransport连接http://127.0.0.1:8000/sse(该地址硬编码于 app.ts 的connectAndLoadApp()),随后即可选择应用并加载。

九、安全设计要点总结

层次防护措施源码依据
沙箱结构双 iframe 代理 + 内层 iframe 的sandbox属性仅开放allow-scripts allow-forms allow-popups allow-modals,禁止顶层导航sandbox.ts
来源校验宿主消息监听校验event.originevent.source;代理校验 referrer 与 iframe 上下文app.ts、sandbox.ts
隔离自检默认执行window.top.alert安全自检,隔离失效则拒绝加载sandbox.ts
工具白名单仅放行visibility"app"的工具,其余以 JSON-RPC 错误-32000拒绝app.ts
内容自包含微应用构建为单文件 HTML,不依赖外部资源server/apps/README.md

十、适用边界与延伸阅读

本文所述宿主是 MCP Apps 规范的参考级实现:它严格遵循标准隔离沙箱容器预期,未引入任何额外自定义托管机制,因而可作为自建 MCP Apps 宿主的可对照范本。需要特别说明的边界包括:工具调用白名单的"未声明即放行"是面向示例的宽松默认(APP_CALLABLE_WHEN_VISIBILITY_UNDECLARED = true),生产宿主可按需收紧为默认拒绝;CORS 全放开与 SSE 地址硬编码同样仅适用于本地开发。

如需继续深入,建议阅读同仓库内的关联材料:

  • 示例整体说明与通信时序:samples/community/mcp/a2ui-in-mcpapps/README.md;
  • 宿主核心实现:app.ts;
  • 服务器工具与资源定义:server.py;
  • 沙箱代理实现:sandbox.ts;
  • 微应用内联构建流程:server/apps/README.md。

【免费下载链接】a2ui项目地址: https://gitcode.com/GitHub_Trending/a2/a2ui

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询