在 MCP Apps 中集成 A2UI:构建基于 Python MCP Server 的交互式 UI 应用服务
【免费下载链接】a2ui项目地址: https://gitcode.com/GitHub_Trending/a2/a2ui
导读
本文基于 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-appMIME 类型,而不仅仅是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 规范的典型三段式消息序列:
dataModelUpdate:声明surfaceId、path: "/"与数据内容(如counter = 0);surfaceUpdate:以组件清单描述 UI 树——Card包住Column,内部依次是Text("Pong from MCP Server (v0.8)!")、Row(含计数器卡片与按钮)。其中按钮通过"action": {"name": "increase_counter", "context": []}把用户点击映射回 MCP 工具调用,而计数文本则通过"text": {"path": "/counter"}绑定数据模型;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:allbuild:all会先执行 Angular 编译,再触发node inline.js把产物内联为单文件public/app.html。
5.2 为什么要单文件内联
由于 MCP App 的安全隔离要求(通常依赖沙箱 iframe,例如srcdoc场景),应用必须是一个不依赖外部请求的独立 HTML 文件。inline.js脚本的工作流程为:
- 收集 Angular 原始构建产物(
dist/raw下的index.html及 JS/CSS); - 把所有 JavaScript 与 CSS 动态内联进
index.html; - 输出自包含的
app.html到public/目录。
以 Editor 应用的 inline.js 为例,实现中还有两个值得注意的细节:
- esbuild 强制打包:Angular 17+ 默认启用 ES Module 代码分割,
main.js会依赖外部相对路径 chunk;而在沙箱srcdociframe 中这些相对请求会被浏览器拦截(缺乏可访问的 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 描述了完整闭环(简化版):
- 宿主从托管服务器加载,向 MCP Server 发
tools/list,得到带_meta.ui.resourceUri(指向ui://模板)的工具定义; - 宿主调用应用入口工具(
tools/call),随后通过resources/read拉取声明的 HTML 模板; - 宿主把模板 HTML 交给沙箱代理,代理在隔离 iframe 中加载 MCP App;
- App 内 CTA 触发后,通过 代理 → 宿主 → Server 的链路转发工具调用;
- Server 返回 A2UI JSON 载荷,经宿主与代理中继后交由 App 内的 A2UI Surface 渲染组件;
- 用户在 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项目地址: https://gitcode.com/GitHub_Trending/a2/a2ui
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考