用 goose 构建真正可用的 MCP Apps渲染机制与五个实战技巧【免费下载链接】goosean open source, extensible AI agent that goes beyond code suggestions - install, execute, edit, and test with any LLM项目地址: https://gitcode.com/GitHub_Trending/goose3/gooseMCP Apps 允许你直接在 AI Agent 的聊天界面里渲染交互式 UI——图表、结账表单、视频播放器都不再是一段枯燥文本。goose 正是最早采用并持续实现 MCP Apps前身 MCP-UI的开源 AI Agent 之一。读完本文你将理解 MCP Apps 在 goose 中的渲染链路掌握适配宿主环境hostContext、分层控制模型可见数据、处理加载与错误态、保持模型上下文同步以及用工具可见性守住高权限操作的五个核心实践并结合 goose 仓库源码reply_parts.rs、extension_manager.rs看清其背后的实现逻辑。MCP Apps 是什么Agent 里的可交互界面MCP Apps官方 MCP 规范中用于交互式 UI 的扩展允许你在任何支持 Model Context Protocol 的 Agent 中直接渲染交互式 UI。它并非「传统 Web 应用」的搬运你的 UI 运行在一个你无法完全掌控的 Agent 里与一个看不见用户交互的模型通信并且要跨多个宿主Host保持原生体验。MCP Apps 起源于实验性项目 MCP-UI在被 goose 等早期客户端采用后由 MCP 维护者纳入了官方扩展如今 goose、MCPJam、Claude、ChatGPT、Postman 等客户端均已支持。在 goose 中这项能力的入口与使用方式在 Using MCP Apps 中有完整说明扩展可以通过 MCP Apps 提供可交互体验App 既可以在独立沙箱窗口中启动侧边栏Apps页、点击Launch也可以由 goose 调用到带 UI 的工具时直接内嵌渲染在对话流中。需要提醒的是goose 文档将其标记为experimental、仍在积极演进行为可能随版本变化。渲染总览工具 → 资源 → iFrame从高层看支持 MCP Apps 的客户端通过 iFrame 加载你的 UI你的 MCP App 暴露一个带有工具tools和资源resources的 MCP server当客户端想加载 App 的 UI 时先调用关联的 MCP 工具再加载包含 HTML 的资源最后把 HTML 装进 iFrame 显示在聊天界面里。以 goose 渲染一份鸡尾酒配方为例完整流程为你向 LLM 提问「给我一份玛格丽特配方」Show me a margarita recipeLLM 以正确参数调用get-cocktail工具——该工具通过_meta.ui.resourceUri声明指向 HTML 资源的 UI 链接客户端用该 URI 获取 MCP resource其中包含该视图View的 HTML 内容HTML 被加载进聊天界面内的 iFrame 中渲染出鸡尾酒配方。goose 的 Rust 端把这一链路落到了工具调用的后处理阶段在 extension_manager.rs 中get_tool_resource_uri负责从工具的meta.0.get(ui)里提取resourceUri字符串而host_supports_mcp_apps见 extension_manager.rs通过宿主声明的 capabilities 判断当前客户端是否支持 MCP Apps依据capabilities.mcpui或 host_info 中的mcpui_enabled()。当支持时hydrate_mcp_app_attachment见 extension_manager.rs会在工具执行结果返回后立即以该resourceUri调用read_resource预取 HTML 内容构造出携带resource_uri、resource_result的GooseMcpAppToolAttachment交给 UI 层注入 iFrame 完成渲染——若资源读取失败错误也会被记录进read_error供客户端展示异常态。视角之外还有不少幕后工作视图水合hydration、能力协商capability negotiation、CSP 策略等。goose 在 goose_apps/resource.rs 中就为 App 资源维护了CspMetadata、PermissionsMetadata、UiMetadata等元数据结构并在 goose_apps/cache.rs 中通过McpAppCache管理已安装与内置bundledApp 的持久化缓存ui://协议由此获得资源寻址能力。建议通读 MCP Apps 官方规范了解完整实现。Tip 1适配宿主环境Adapt to the Host Environment构建 MCP App 时你要让它像 Agent 体验的自然组成部分而不是生硬拼装上去的组件。视觉落差是破坏这种错觉最快的因素——用户在一个深色模式的 Agent 里启动 MCP AppApp 却以刺眼的浅色渲染即使功能完全正常体验也立刻「出戏」。默认情况下你的 MCP App 对周边 Agent 环境一无所知因为它运行在沙箱 iFrame 中无法判断 Agent 处于深色还是浅色模式、视口多大、用户偏好哪种 locale。解决办法是宿主Host即 Agent与视图View即你的 App之间的环境共享当 View 连接时它发送ui/initialize请求Host 以hostContext对象响应描述当前环境当主题、视口或 locale 等发生变化时Host 发送只含变更字段的ui/notifications/host-context-changed通知。两者间的对话大致如下View「我在初始化。你的环境长什么样」Host「我们处于深色模式视口 400×300locale 是 en-US运行在桌面端。」用户切换到浅色主题Host「更新现在是浅色模式了。」作为开发者你的职责就是让 MCP App 真正消费hostContext并据此适配环境。在 MCP App 中使用 hostContextimport { useState } from react; import { useApp } from modelcontextprotocol/ext-apps/react; import type { McpUiHostContext } from modelcontextprotocol/ext-apps; function MyApp() { const [hostContext, setHostContext] useStateMcpUiHostContext | undefined(undefined); const { app, isConnected, error } useApp({ appInfo: { name: MyApp, version: 1.0.0 }, capabilities: {}, onAppCreated: (app) { app.onhostcontextchanged (ctx) { setHostContext((prev) ({ ...prev, ...ctx })); }; }, }); if (error) return divError: {error.message}/div; if (!isConnected) return divConnecting.../div; return ( div pTheme: {hostContext?.theme}/p pLocale: {hostContext?.locale}/p pViewport: {hostContext?.containerDimensions?.width} x {hostContext?.containerDimensions?.height}/p pPlatform: {hostContext?.platform}/p /div ); }:::tip 使用useApphook 时它会提供onhostcontextchanged监听器。配合 React 的useState即可更新 App 上下文。宿主给出的是它的环境描述至于要拿它做什么由作为 App 开发者的你决定例如用 theme 渲染浅/深色模式、用 locale 切换显示语言、用 containerDimensions 自适应尺寸。 :::Tip 2控制模型看到什么、View 看到什么有些场景下你需要粒度化地控制 LLM 能访问哪些数据、View 能展示哪些数据。MCP Apps 规范为此规定了三种工具返回值宿主对它们各不相同的处理方式实现了数据流的隔离content暴露给模型的信息为模型提供上下文structuredContent对模型上下文隐藏专门用于把数据发给 View 做水合hydration_meta对模型上下文隐藏用于携带时间戳、版本信息等附加信息。用一个鸡尾酒 App 的实践案例说明三者如何协同server.registerTool( view-cocktail, { title: Get Cocktail, description: Fetch a cocktail by id with ingredients and images..., inputSchema: z.object({ id: z.string().describe(The id of the cocktail to fetch.) }), _meta: { ui: { resourceUri: ui://cocktail/cocktail-recipe-widget.html }, }, }, async ({ id }: { id: string }): PromiseCallToolResult { const cocktail await convexClient.query(api.cocktails.getCocktailById, { id, }); return { content: [ { type: text, text: Loaded cocktail ${cocktail.name}. }, { type: text, text: Cocktail ingredients: ${cocktail.ingredients}. }, { type: text, text: Cocktail instructions: ${cocktail.instructions}. }, ], structuredContent: { cocktail }, _meta: { timestamp: new Date().toString() } }; }, );这个工具渲染一份鸡尾酒配方视图。数据从后端数据库Convex取出View 需要完整的鸡尾酒数据因此通过structuredContent传递而模型不需要图片 URL 这类完整数据只需知道名称、配料与步骤等要点这部分经content给出。需要特别注意ChatGPT apps SDK 当前的实现不同——它把structuredContent同时暴露给模型与 Viewcontent暴露给模型提供上下文structuredContent同时暴露给模型和 View_meta对模型上下文隐藏。如果你的 App 需要同时支持 MCP Apps 与 ChatGPT apps SDK这个差异很关键——你可能需要按返回值条件分支或根据客户端是 MCP App 支持还是 ChatGPT App 来条件渲染工具。Tip 3妥善处理加载态与错误态iFrame 通常先于工具执行完成、View 水合之前渲染出来。此时你应该用漂亮的加载态告诉用户 App 正在加载。这里有一个值得注意的强力特性toolInputs会先于工具执行结束被发送并流式进入 View。这让你能做出很酷的「部分加载态」——数据仍在抓取时就能向用户展示正在请求的内容。还是以鸡尾酒 App 为例MCP 工具抓取鸡尾酒数据并经structuredContent传给 View但抓取耗时未知运气差时从几毫秒到几秒都有可能server.registerTool( view-cocktail, { title: Get Cocktail, description: Fetch a cocktail by id with ingredients and images..., inputSchema: z.object({ id: z.string().describe(The id of the cocktail to fetch.) }), _meta: { ui: { resourceUri: ui://cocktail/cocktail-recipe-widget.html, visibility: [model, app], }, }, }, async ({ id }: { id: string }): PromiseCallToolResult { const cocktail await convexClient.query(api.cocktails.getCocktailById, { id, }); return { content: [ { type: text, text: Loaded cocktail ${cocktail.name}. }, ], structuredContent: { cocktail }, }; }, );在 View 侧ReactuseAppAppBridge hook 提供app.ontoolresult监听器负责接收工具返回结果并水合 View。在onToolResult尚未到达、数据为空时就可以渲染漂亮的加载态import { useApp } from modelcontextprotocol/ext-apps/react; function CocktailApp() { const [cocktail, setCocktail] useStateCocktailData | null(null); useApp({ appInfo: IMPLEMENTATION, capabilities: {}, onAppCreated: (app) { app.ontoolresult async (result) { const data extractCocktail(result); setCocktail(data); }; }, }); return cocktail ? CocktailView cocktail{cocktail} / : CocktailViewLoading /; }错误态处理错误同样要优雅处理。当工具内部出错例如鸡尾酒数据加载失败时LLM 和 View 都应当收到错误通知。在 MCP 工具中你应该在工具结果里返回error字段——它既暴露给模型也会传给 Viewserver.registerTool( view-cocktail, { title: Get Cocktail, description: Fetch a cocktail by id with ingredients and images..., inputSchema: z.object({ id: z.string().describe(The id of the cocktail to fetch.) }), _meta: { ui: { resourceUri: ui://cocktail/cocktail-recipe-widget.html }, visibility: [model, app], }, }, async ({ id }: { id: string }): PromiseCallToolResult { try { const cocktail await convexClient.query(api.cocktails.getCocktailById, { id, }); return { content: [ { type: text, text: Loaded cocktail ${cocktail.name}. }, ], structuredContent: { cocktail }, }; } catch (error) { return { content: [ { type: text, text: Could not load cocktail }, ], error }; } }, );随后在 React 客户端一侧的useApp里只需检查工具结果中是否存在error字段即可感知错误。这条链路与 goose 的预取设计互为印证如前所述goose 在hydrate_mcp_app_attachment中若资源读取失败会填充read_errorUI 侧据此同样可以进入错误分支。Tip 4让模型保持在环Keep the Model in the Loop你的 MCP App 运行在沙箱 iFrame 里默认情况下驱动 Agent 的模型看不到 App 内部发生的一切——它不知道用户是否填了表单、点了按钮、完成了购买。没有反馈回路模型就会丢失上下文。用户买完一双鞋后问「什么时候发货」模型甚至不知道交易发生过。为此SDK 提供两个方法让模型与用户的旅程保持同步sendMessage与updateModelContext。sendMessage()用于主动触发。它像用户亲自输入那样给模型发送一条消息促使模型立即响应。非常适合在用户点击「购买」后立即确认或在一个动作完成后立刻推荐相关商品// 用户点击 Buy —— 模型立即响应 await app.sendMessage({ role: user, content: [{ type: text, text: I just purchased Nike Air Max for $129 }], }); // 结果模型回复 Great choice! Want me to track your order?updateModelContext()用于后台感知。它安静地把信息保存下来供模型稍后使用不打断当前流程。适合追踪浏览历史或购物车变化而不必每次都触发一条聊天回复// 用户在浏览 —— 无需立即响应 await app.updateModelContext({ content: [{ type: text, text: User is viewing: Nike Air Max, Size 10, $129 }], }); // 结果无响应。但如果用户稍后问 我刚才在看什么模型知道答案。Tip 5控制谁能触发工具Control Who Can Trigger Tools在标准 MCP server 中模型看到你的工具、解读用户提示、再调用正确的工具。用户说「删掉那封邮件」由模型决定这句话的含义并调用删除工具。而在 MCP App 中工具存在两种触发途径模型解读用户提示后调用或者用户直接在 UI 上交互触发。默认情况下两者都能调用任意工具。举例假设你构建了一个把邮件收件箱可视化呈现、允许用户直接操作邮件的 MCP App。现在你的工具有两个潜在触发者模型响应「删除我的旧邮件」这类提示而调用删除用户直接在 App 界面点击删除按钮。模型的本质是解读意图。用户说「删除我的旧邮件」时模型必须自行判断「旧」指什么、哪些邮件符合条件。对于删邮件这类动作这种歧义可能带来风险而用户在你的 MCP App 中点开某封具体邮件旁边的「删除」按钮时则毫无歧义——他们做了明确选择。要防止模型基于误解误执行高利害动作你可以用tool visibility把某些工具限制为只能由 MCP App 的 UI 调用。模型负责展示界面最终动作必须由真人点击确认。visibility 有三种配置[model, app]默认——模型与 UI 均可调用[model]——只有模型能调用UI 不能[app]——只有 UI 能调用对模型隐藏。实现示例如下// 模型调用它以展示收件箱 registerAppTool(server, show-inbox, { description: Display the users inbox, _meta: { ui: { resourceUri: ui://email/inbox.html, visibility: [model], }, }, }, async () { const emails await getEmails(); return { content: [{ type: text, text: JSON.stringify(emails) }] }; }); // 用户在 UI 中点击删除按钮 registerAppTool(server, delete-email, { description: Delete an email, inputSchema: { emailId: z.string() }, _meta: { ui: { resourceUri: ui://email/inbox.html, visibility: [app], }, }, }, async ({ emailId }) { await deleteEmail(emailId); return { content: [{ type: text, text: Email deleted }] }; });delete-email因为visibility: [app]而不会被模型感知从而杜绝了「删除」语义被模型猜错的风险。这一设计在 goose 源码中有直接的执行印证在 reply_parts.rs 中is_tool_visible_to_model与is_tool_visible_to_app按 2026-01-26 版 MCP Apps 规范实现了判断逻辑——_meta.ui.visibility缺失时默认双端可见不含model即视为 app-only模型不可见不含app即 model-onlyUI 不可调用。工具在发往 LLM 前会经过tools.retain(is_tool_visible_to_model)过滤见 reply_parts.rs被过滤掉app-only的工具根本不会出现在模型面前从源头规避了误触发。仓库中还有大量围绕这一机制的单元测试例如 extension_manager.rs 用get_tool_resource_uri分区验证 MCP App 工具与普通工具测试注释直接写明「non-MCP-app tools have no resourceUri」——它同时确认了判定一个工具是否属于 MCP App正是看其_meta.ui里是否存在resourceUri。在 goose 中开始构建与测试MCP Apps 为 Agent 交互打开了新维度。goose 这一侧你既能测试也能运行自己的 App本地调试可以先用 MCPJam面向 MCP Apps、ChatGPT apps SDK 与 MCP server 的开源本地 inspector反复调试、迭代再正式发布——它非常适合在交付前打磨你的 App在 goose 中运行goose 作为开源 AI Agent会把 MCP Apps 直接渲染到聊天界面中让你在真实的 Agent 环境里看到 App 活起来。按 Using MCP Apps 的描述装有支持的扩展后既可以从侧边栏Apps页把 App 启动到独立沙箱窗口也可以让 goose 在调用到带 UI 的工具时把界面内嵌进对话流参考内置范例如果你需要一份「goose 内真实的 MCP App 扩展」作为模板可以研读 autovisualiser 扩展的注册与前端桥接实现crates/goose-mcp/src/autovisualiser/mod.rs其工具即通过_meta.ui中的resourceUri形如ui://autovisualiser/chart见 extension_manager.rs向宿主声明可渲染的图表视图并附带mcp-app-bridge.js这类前端桥接资源是理解规范落地的绝佳样本官方教程goose 文档提供了从零开始的完整实战引导——Building MCP Apps按步骤即可构建你的第一个 MCP App也可以访问 Custom Extensions 了解扩展体系并在 autovisualiser 的 MCP 说明 中看到交互式 UI 扩展的完整使用姿势。动手时请始终把「最终运行在你不控制的 Agent 里」作为默认假设适配hostContext、分层掌控模型与视图的数据边界、用心打磨加载与错误态、用sendMessage/updateModelContext把用户每一步操作同步回模型并用visibility把高风险动作锁在人类点击之后——这五点正是让 MCP App 从一个「能渲染的页面」进化为「真正可用的 Agent 应用」的分水岭。【免费下载链接】goosean open source, extensible AI agent that goes beyond code suggestions - install, execute, edit, and test with any LLM项目地址: https://gitcode.com/GitHub_Trending/goose3/goose创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考