在 MCP Apps 中集成 A2UI:构建基于 Python MCP Server 的交互式 UI 应用服务

原创2026-09-14 18:59:081,873 阅读
文章标签:人工智能AI AgentAI 应用前端UI组件

在 MCP Apps 中集成 A2UI:构建基于 Python MCP Server 的交互式 UI 应用服务

导读

本文基于 a2ui 仓库中的 samples/community/mcp/a2ui-in-mcpapps/server 示例,系统讲解如何用 Python 编写一个 Model Context Protocol(MCP)服务器,将独立打包的 Web 应用作为 MCP App 资源对外提供,并通过 MCP 工具(tools)把原始的 A2UI JSON 载荷直接翻译为丰富的交互式 UI 渲染。读完本文,你将掌握 MCP Server 中 resources 与 tools 的声明方式、_meta.ui.resourceUri 的 UI 模板关联机制、SSE/Stdio 双传输通道的启动方式,以及 A2UI 载荷(dataModelUpdate / surfaceUpdate / beginRendering)如何驱动界面增量更新。

一、示例概览:MCP 生态中的 A2UI 承载方式

本示例位于仓库的 samples/community/mcp/a2ui-in-mcpapps,整体分为三个部分:

  • server/:Python + uv 构建的 MCP Server,对外提供 micro-app 资源与交互工具,是本文的主角;
  • client/:Angular 编写的宿主容器应用,通过安全的双 iframe 代理模式加载并隔离运行来自 MCP 的微应用;
  • server/apps/:被托管的微应用源码(Basic 计数器应用与 Editor 生成式文档编辑器),构建为单文件 HTML 后交由 MCP Server 对外提供。

其中,server 端目录的核心文件与职责如下(见 server/README.md):

文件/目录 职责
server.py MCP Server 核心实现,定义 tools、resources 与传输方式(SSE / Stdio)
simple_counter_a2ui.json 示例 A2UI 载荷数据文件,供 fetch_counter_a2ui 工具返回
apps/ 被托管应用(hosted application)的源码与构建产物目录
apps/public/app.html Server 实际对外服务的自包含单文件应用(需先构建生成)

关键约束:该 Server 专门期望服务位于 apps/public/app.html 的打包产物;该文件缺失或源码变更后,必须先重新构建(详见下文"托管应用构建"一节)。

二、暴露的接口:Resources 与 Tools

MCP Server 通过 resources/read 提供 MCP App 的 HTML 模板,通过 tools/call 返回 A2UI 载荷来驱动界面渲染。

2.1 Resources(资源)

  • ui://basic/app:对外提供自包含的 apps/public/app.html 应用,MIME 类型为 text/html;profile=mcp-app。

在 server.py 中,list_resources 除 basic 外还声明了第二个资源 ui://editor/app(Editor 应用),并且 read_resource 会按 URI 映射到 apps/public/ 下对应的 app.html / editor.html 文件。这里有一个关键实现细节:resources/read 的返回内容必须携带 text/html;profile=mcp-app MIME 类型,而不仅仅是 resources/list 声明时带上——这是 MCP Apps 规范对资源读取的强制要求,也是本示例特意在代码注释中强调的点。

2.2 Tools(工具)

基础计数器相关工具(对应 simple_counter_a2ui.json):

  • get_basic_app:返回 ui://basic/app 资源的引用。该工具通过 _meta.ui.resourceUri 预声明 UI 模板,宿主通过 resources/read 获取模板,而不会把模板作为内嵌资源混进工具结果中(见 server.py);
  • fetch_counter_a2ui:读取 simple_counter_a2ui.json,返回初始计数器 A2UI 载荷用于测试渲染;
  • increase_counter:对内存计数器自增并返回标准的 dataModelUpdate,实现 UI 组件更新。

生成式编辑器相关工具(同一 server 文件中的扩展工具):

  • get_editor_app:打开 Editor A2UI 应用视图(通过 _meta.ui.resourceUri 关联 ui://editor/app);
  • smart_editor_get_controls:根据用户选中的文本,让 Gemini 生成 A2UI 调参控件(slider / checkbox / select);
  • smart_editor_apply:把用户在控件上调整后的参数提交给 Gemini 重写文本。

每个工具都通过 _meta.ui.visibility 声明可见性(如 ["model"] 仅模型可见、["app"] 允许应用调用)。需要说明的是,这些 Editor 扩展工具属于仓库中同一 server 代码的一部分,是理解"服务端如何为 MCP App 提供完整交互闭环"的重要补充,但计数器工具才是本文主体 README 所聚焦的基础演示。

三、快速开始:运行 MCP Server

3.1 前置条件

  • Python 3.10+;
  • uv(推荐的 Python 包管理工具,可依据 server/.python-version 自动管理 Python 版本)。

3.2 方式 A:SSE 传输(默认)

在 server/ 目录下启动,默认监听 127.0.0.1:8000 等待 SSE 连接:

cd samples/community/mcp/a2ui-in-mcpapps/server
uv run python server.py --transport sse --port 8000

由于程序默认参数就是 sse 与端口 8000,也可以直接简写为:

uv run python server.py

首次运行前建议先执行 uv sync 按 pyproject.toml 安装依赖(click、mcp[cli]、sse-starlette、starlette、uvicorn、google-genai、python-dotenv 等)。

3.3 方式 B:Stdio 传输

使用标准输入输出与宿主进程通信,适合作为子进程被 Agent 宿主拉起:

uv run python server.py --transport stdio

传输方式的切换由 server.py 中的 click 参数控制:

@click.command()
@click.option("--port", default=8000, help="Port to listen on for SSE")
@click.option(
    "--transport",
    type=click.Choice(["stdio", "sse"]),
    default="sse",
    help="Transport type",
)

3.4 两种传输的底层实现差异

  • SSE:基于 Starlette + Uvicorn 搭建 HTTP 服务,Route("/sse") 处理事件流连接,Mount("/messages/") 接收客户端 POST 消息;同时注册了 CORSMiddleware(源码中带有明确警告:生产环境必须将 allow_origins=["*"] 收紧为宿主客户端的具体来源,例如 http://localhost:4200);
  • Stdio:通过 mcp.server.stdio.stdio_server 在标准输入输出上建立双向消息流,并用 anyio.run 驱动事件循环。

四、深入源码:工具调用如何返回 A2UI 载荷

handle_call_tool 是界面驱动的核心,其返回的 CallToolResult 中内嵌了 MIME 类型为 application/a2ui+json(源码常量 A2UI_MIME_TYPE)的 TextResourceContents(见 server.py):

  • get_basic_app / fetch_counter_a2ui:把 simple_counter_a2ui.json 的内容序列化后,以 a2ui://ping-result 为 URI 的内嵌资源返回;
  • increase_counter:修改全局计数器后,构造一个 dataModelUpdate 消息,通过 surfaceId: "ping-result"、contents 中的 {"key": "counter", "valueNumber": COUNTER} 精准更新数据模型中指定 key 的值,前端据此重新渲染 score-value 文本。

这种"工具结果携带 A2UI JSON"的方式,让 Agent 宿主可以直接把载荷交给 A2UI 渲染层解析,实现一次工具调用即完成一次界面更新。

4.1 初始 A2UI 载荷剖析

simple_counter_a2ui.json(见 simple_counter_a2ui.json)是 A2UI v0.8 规范的典型三段式消息序列:

  1. dataModelUpdate:声明 surfaceId、path: "/" 与数据内容(如 counter = 0);
  2. surfaceUpdate:以组件清单描述 UI 树——Card 包住 Column,内部依次是 Text("Pong from MCP Server (v0.8)!")、Row(含计数器卡片与按钮)。其中按钮通过 "action": {"name": "increase_counter", "context": []} 把用户点击映射回 MCP 工具调用,而计数文本则通过 "text": {"path": "/counter"} 绑定数据模型;
  3. beginRendering:指定 root: "root" 作为渲染入口,通知渲染器开始绘制该 surface。

这一结构清楚展示了 A2UI 的"数据模型 + 组件树 + 渲染指令"分离设计,以及 action 与数据绑定如何支撑起完整的交互闭环。

五、托管应用的构建要求

Server 对外服务的是 apps/public/app.html 这个自包含单文件产物。若该文件缺失,或你修改了 apps/src/ 下的托管应用源码,都必须重新构建(详见 apps/README.md)。

5.1 构建工作流

在 server/apps/src/ 目录下执行:

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

build:all 会先执行 Angular 编译,再触发 node inline.js 把产物内联为单文件 public/app.html。

5.2 为什么要单文件内联

由于 MCP App 的安全隔离要求(通常依赖沙箱 iframe,例如 srcdoc 场景),应用必须是一个不依赖外部请求的独立 HTML 文件。inline.js 脚本的工作流程为:

  1. 收集 Angular 原始构建产物(dist/raw 下的 index.html 及 JS/CSS);
  2. 把所有 JavaScript 与 CSS 动态内联进 index.html;
  3. 输出自包含的 app.html 到 public/ 目录。

以 Editor 应用的 inline.js 为例,实现中还有两个值得注意的细节:

  • esbuild 强制打包:Angular 17+ 默认启用 ES Module 代码分割,main.js 会依赖外部相对路径 chunk;而在沙箱 srcdoc iframe 中这些相对请求会被浏览器拦截(缺乏可访问的 base origin),因此脚本调用 npx esbuild --bundle --format=esm 将所有 split chunks 合并为单一文件后再内嵌;
  • 清理 modulepreload:Angular 自动注入的 <link rel="modulepreload"> 在强制打包后会产生无意义的 404/CORS 网络错误,脚本会将其全部剔除,同时剥离 sourceMappingURL 以减小体积。

5.3 构建产物与 Git 忽略

dist/(原始构建输出)与 public/(最终打包产物)均被 git 忽略:全新 clone 的仓库默认没有这些产物,Server 即使没有它们也能启动,但对应 surface 无法加载。因此在端到端运行示例前,至少需要构建一个应用。仓库根目录的 样例总览 提供了 Editor 与 Basic 两种应用的构建命令,以及先 yarn install 链接工作区包的提醒。

六、端到端运行与完整通信流程

6.1 启动宿主客户端

除 Server 外,还需要构建宿主容器(Angular Client)的沙箱桥接资源并在 4200 端口启动:

cd samples/community/mcp/a2ui-in-mcpapps/client
yarn install
yarn build:sandbox      # 生成 client/public/sandbox_iframe/sandbox.{js,html}
yarn start              # 打开 http://localhost:4200 查看运行中的宿主

6.2 消息流转时序

样例总览文档用 sequence diagram 描述了完整闭环(简化版):

  1. 宿主从托管服务器加载,向 MCP Server 发 tools/list,得到带 _meta.ui.resourceUri(指向 ui:// 模板)的工具定义;
  2. 宿主调用应用入口工具(tools/call),随后通过 resources/read 拉取声明的 HTML 模板;
  3. 宿主把模板 HTML 交给沙箱代理,代理在隔离 iframe 中加载 MCP App;
  4. App 内 CTA 触发后,通过 代理 → 宿主 → Server 的链路转发工具调用;
  5. Server 返回 A2UI JSON 载荷,经宿主与代理中继后交由 App 内的 A2UI Surface 渲染组件;
  6. 用户在 A2UI 组件上点击时,action 被映射为 tools/call 请求,再走同一链路回传 dataModelUpdate,最终完成增量渲染更新。

宿主侧的 client/src/app/app.ts 中可见其实现要点:监听 window message 事件并校验 event.origin 与 event.source(安全边界),按每个工具声明的 _meta.ui.visibility 构建 allowedTools 集合,并通过 ui/notifications/sandbox-proxy-ready、ui/notifications/sandbox-resource-ready 等约定消息与沙箱代理握手。

七、扩展:生成式文档编辑器中的 A2UI 动态控件

除基础计数器外,同一 Server 还演示了"LLM 动态生成 A2UI 控件"的高级用法(实现于 smart_editor_agent.py):

  • generate_controls(text, full_text):把选中文本交给 Gemini(默认模型 gemini-2.5-flash,可通过 GENAI_MODEL 环境变量覆盖),通过 response_schema 约束输出 JSON,得到 2~3 个调参控件;随后把控件映射为 A2UI 组件——Slider(0–100 数值)、CheckBox(布尔值)、MultipleChoice(下拉选项),连同 dataModelUpdate、surfaceUpdate、beginRendering 三段消息返回给前端;
  • apply_revision(text, user_parameters):解析 control_config_json 中的控件定义,把用户当前值格式化为自然语言指令(如 - Verbose vs. Concise: 0.30 (0 meaning low...)),再要求 Gemini 输出 text_before / original_text / revised_text / text_after 四段式修订结果,实现基于用户调参的文本重写。

该扩展说明:MCP Server 返回的 A2UI 载荷不必是静态 JSON——服务端可以借助 LLM 按上下文实时生成界面结构,这正是 A2UI 面向 Agent 生态的灵活之处。

八、总结与排查建议

  • 启动失败:确认 Python ≥ 3.10,且已 uv sync;SSE 模式下可开启日志观察连接建立(源码中 logging.basicConfig(level=logging.INFO) 就是为此准备的);
  • surface 加载空白:检查 apps/public/app.html 是否已构建,Server 在 read_resource 找不到文件时会抛出 ValueError: Resource file not found...;
  • 跨域问题:本地调试允许 CORS *,但生产环境务必按源码警告收紧 allow_origins;
  • 换用 UI 模板:新增应用时,需同时修改 list_resources、read_resource 中的 URI 映射,以及对应工具声明的 _meta.ui.resourceUri。

本示例的价值在于给出了一个可运行的最小参考实现:从资源声明、工具返回 A2UI 载荷、单文件应用构建,到宿主沙箱隔离与增量渲染,完整串联了 MCP Apps 与 A2UI 的集成路径。相关可继续研读的仓库文件包括:server.py、simple_counter_a2ui.json、apps/README.md 与 样例总览。

登录后查看全文
a2ui