MCP Apps 扩展实战:用 python-sdk 为工具打造交互式界面 MCP Apps 扩展实战用 python-sdk 为工具打造交互式界面【免费下载链接】python-sdkThe official Python SDK for Model Context Protocol servers and clients项目地址: https://gitcode.com/gh_mirrors/pythonsd/python-sdkMCP Apps 是 Model Context Protocol 官方 Python SDKpython-sdk内置的扩展能力扩展标识io.modelcontextprotocol/ui它让一个普通工具额外携带一份 HTML 文档由宿主Host在沙箱 iframe 中渲染成交互界面。读完本文你将掌握Apps扩展的完整使用链路如何注册 UI 绑定的工具与ui://资源、如何优雅降级兼容纯文本客户端、如何用 CSP 与浏览器权限锁定 iframe以及 SDK 在启动期强制校验的规则。本页面对应英文原文档 docs/advanced/apps.md并辅以源码与测试作证据支撑。什么是 MCP App一个有脸的工具MCP App 是一个带有脸UI的工具除了像普通工具一样返回数据外它还指向一份 HTML 文档宿主将其渲染为可交互的表面。这个概念始终由两部分组成缺一不可一个工具tool——负责干活并返回数据与任何其他工具无异一个ui://资源resource——包含宿主为该工具展示的 HTML。两者如何关联工具上携带_meta.ui.resourceUri引用指向该资源。宿主的完整动作是用resources/read获取该资源在**沙箱 iframesandboxed iframe**中渲染它通过postMessage把工具的执行结果推送到 iframe 内。一个关键设计是你的服务端从不发送或接收任何ui/*消息——这部分流量完全发生在宿主与 iframe 之间。你只需提供一个工具和一份 HTML 文档其余演出theater由宿主负责。在 SDK 中这一切以内置的Apps扩展形式提供扩展标识为io.modelcontextprotocol/ui。扩展机制遵循 SEP-2133 契约扩展默认关闭off by default服务端通过向MCPServer(extensions[...])传入实例显式启用启用后服务端会在capabilities.extensions下公告该扩展。如果你还不熟悉扩展机制建议先阅读 扩展机制总览或英文版 docs/advanced/extensions.md再回到本页。扩展在构造时固定没有运行期追加的add_extension因为连接期间能力表不应变化。最小示例一个有脸的时钟以官方教程 docs_src/apps/tutorial001.py 为例完整代码如下from mcp.server.apps import Apps, client_supports_apps from mcp.server.mcpserver import MCPServer from mcp.server.mcpserver.context import Context CLOCK_HTML \ !doctype html titleClock/title h1 idnow.../h1 script window.addEventListener(message, (event) { const text event.data?.result?.content?.[0]?.text; if (text) document.getElementById(now).textContent text; }); /script apps Apps() apps.tool(resource_uriui://clock/app.html, descriptionThe current time.) def get_time(ctx: Context) - str: now 2026-06-26T12:00:00Z if not client_supports_apps(ctx): return fThe time is {now}. return now apps.add_html_resource(ui://clock/app.html, CLOCK_HTML, titleClock) mcp MCPServer(clock, extensions[apps])四个关键动作Apps()创建一个实例它同时持有你 UI 绑定的工具和它们对应的资源。从源码src/mcp/server/apps.py可以看到实例内部用两个列表分别存放(ToolBinding, resource_uri)元组与ResourceBinding。apps.tool(resource_uriui://clock/app.html)一个普通工具外加_meta.ui.resourceUri印记。mcp.tool()接受的一切参数name、title、description、annotations等都原样透传tool_kwargs会被转发给MCPServer.add_tool。apps.add_html_resource(ui://clock/app.html, CLOCK_HTML)注册匹配的资源以text/html;profilemcp-app这一 MIME 类型提供。正是这个精确的 MIME 类型告诉宿主这是一个应用请渲染它。该方法还支持name、title、description、csp、permissions、domain、prefers_border等参数详见下文 iframe 安全小节。MCPServer(clock, extensions[apps])显式启用。服务端随即在capabilities.extensions下公告io.modelcontextprotocol/ui。HTML 本身监听宿主的postMessage事件并展示结果。对于真实应用建议在 HTML 内使用官方modelcontextprotocol/ext-apps浏览器 SDK它替你封装了原始消息事件直接提供ontoolresult、callServerTool、getHostContext、onhostcontextchanged等高层 API。服务端是两件套tools()方法会在服务器消费扩展时校验每个工具的resource_uri是否已注册了匹配资源详见下文SDK 强制执行的规则resources()则返回注册的资源绑定。优雅降级一个工具两种回答并非每个客户端都会渲染应用。规范对此非常直白这是必须遵守的硬性要求即使 UI 可用工具**也必须MUST**返回有意义的content数组。原因在于模型读的是contentiframe 是给人类看的。支持 UI 的宿主仍会把文本结果喂给模型而纯文本客户端只能拿到文本。因此规范范式是一个工具两种回答。再看一眼get_timeapps.tool(resource_uriui://clock/app.html, descriptionThe current time.) def get_time(ctx: Context) - str: now 2026-06-26T12:00:00Z if not client_supports_apps(ctx): return fThe time is {now}. return nowclient_supports_apps(ctx)的判定条件是客户端声明了io.modelcontextprotocol/ui扩展并且在其mimeTypes设置中列出了text/html;profilemcp-app。注意mimeTypes是必填字段——省略它的客户端不计入支持。从源码src/mcp/server/apps.py可以看到该函数读取客户端能力中的extensions[EXTENSION_ID]然后检查mimeTypes是否为 list/tuple 且包含APP_MIME_TYPE。它在高层Context与底层ServerRequestContext两种形态下都可用_client_capabilities内部做了分支处理。协商的客户端一侧docs_src/apps/tutorial001_client.py如下import anyio from mcp import Client from mcp.client import advertise from mcp.server.apps import APP_MIME_TYPE, EXTENSION_ID from mcp.types import TextContent APPS_SUPPORT advertise(EXTENSION_ID, {mimeTypes: [APP_MIME_TYPE]}) async def main() - None: async with Client(http://localhost:8000/mcp, extensions[APPS_SUPPORT]) as client: result await client.call_tool(get_time, {}) for block in result.content: if isinstance(block, TextContent): print(block.text) if __name__ __main__: anyio.run(main)通过 HTTP 提供server.py然后在第二个终端运行客户端uv run mcp run server.py --transport streamable-httppython client.py2026-06-26T12:00:00Z富结果返回了。如果把Client调用中的extensions[APPS_SUPPORT]参数去掉同一个程序会改而打印The time is 2026-06-26T12:00:00Z.——这正是纯文本客户端能看到的一切。测试 tests/docs_src/test_apps.py 在进程内同时驱动两种客户端验证了同一工具的两种回答路径。!!! warning 永远不要把[Rendered UI]之类的占位符作为唯一内容返回。如果后备文本没用这个工具对每个纯文本客户端、对模型本身都没用。把那句人话写出来。关于协商的边界测试 tests/server/test_apps.py 用参数化用例钉死了client_supports_apps的判定矩阵列出mimeTypeslist 或 tuple 皆可返回True扩展未声明、MIME 不是text/html;profilemcp-app、或mimeTypes键缺失均返回False。另外需要注意扩展能力表经由server/discover2026-07-28 协议路径传递legacyinitialize握手无处安放它因此 legacy 客户端看不到该扩展——这也是设计上扩展必须优雅降级、不能成为服务唯一可用方式的原因见 tests/server/test_apps.py。锁住 iframeCSP 与权限声明安全元数据放在资源侧iframe 能加载什么、想要哪些浏览器权限、希望如何被框定。官方教程 docs_src/apps/tutorial002.py 演示了完整形态from mcp.server.apps import Apps, ResourceCsp, ResourcePermissions from mcp.server.mcpserver import MCPServer DASHBOARD_HTML !doctype htmltitleDashboard/titlecanvas idchart/canvas apps Apps() apps.tool(resource_uriui://dashboard/app.html, visibility[app]) def refresh_dashboard() - str: Refresh the dashboard data. return refreshed apps.add_html_resource( ui://dashboard/app.html, DASHBOARD_HTML, titleDashboard, cspResourceCsp(connect_domains[https://api.example.com]), permissionsResourcePermissions(clipboard_write{}), domaindashboard.example.com, prefers_borderTrue, ) mcp MCPServer(dashboard, extensions[apps])要澄清一个根本定位csp和permissions是对宿主的请求requests不是服务端行为。宿主根据它们构建 iframe 的 Content-Security-Policy 和 Permissions-Policy并且可以拒绝。所以在你的 JS 代码里要做特性检测feature-detect而不要假设授权一定被批准。ResourceCsp逐字段说明Python 名称 → 传输键 → 宿主用它控制什么Python传输键_meta.ui.csp控制内容connect_domainsconnectDomainsconnect-srcfetch/XHR 可以访问哪些域resource_domainsresourceDomainsimg-src、style-src等静态资源来源frame_domainsframeDomainsframe-src嵌套 iframe 来源base_uri_domainsbaseUriDomainsbase-uribase可以指向哪里ResourcePermissions的每个字段都向宿主请求一项 iframe 浏览器权限Python传输键_meta.ui.permissionscameracameramicrophonemicrophonegeolocationgeolocationclipboard_writeclipboardWrite从源码src/mcp/server/apps.py可见两个模型类都基于 pydantic通过alias_generatorto_camel自动生成驼峰式传输键model_dump(by_aliasTrue)序列化。add_html_resource会把csp、permissions、domain、prefers_border一并汇入资源的_meta.ui测试 tests/server/test_apps.py 验证了这些字段同时落在resources/list条目与resources/read内容项的_meta.ui中便于宿主读取。!!! note CSP 和权限属于资源resource绝不属于工具tool。规范的工具元数据中没有它们的槽位宿主也会忽略放在工具上的值。SDK 直接让这种错误不可表达apps.tool()根本没有csp参数。可见性Visibility工具上的visibility[app]表示这个工具是给 iframe 用的不是给模型用的。可选值model模型可以调用它appiframe 可以调用它通过callServerTool省略两者皆可这是默认值。过滤是宿主的职责。你的服务端会在tools/list中像列其他工具一样列出 app-only 工具测试 tests/docs_src/test_apps.py 验证了它仍可被调用并返回refreshed由宿主把它们对模型隐藏。不要在服务端做过滤。visibility会被写入_meta.ui.visibility见 src/mcp/server/apps.py 与 tests/server/test_apps.py。SDK 强制执行的规则启动期报错而不是生产期以下所有规则在启动时startup报错而不是在生产运行期失败非ui://...的resource_uri或资源 URI在装饰/注册那一刻抛出ValueError。_require_ui_scheme用uri.startswith(ui://)校验src/mcp/server/apps.py测试 tests/server/test_apps.py 分别覆盖了apps.tool()与add_html_resource()两个入口。绑定了无匹配注册资源的 URI当MCPServer(extensions[apps])消费该扩展时抛出ValueError。一个公告的 HTML 在resources/read上 404 属于配置错误因此拒绝构造。tools()方法在返回绑定前遍历比对已注册资源集合src/mcp/server/apps.py错误消息形如Apps tool _widget binds resource_uri ui://missing/app.html, but no such resource is registered; add it with add_html_resource() or add_resource()见 tests/server/test_apps.py。apps.tool()上传递meta{ui: ...}抛出ValueError。装饰器独享_meta[ui]的所有权请用resource_uri和visibility表达其他meta键可以与之和平共存地合并测试 tests/server/test_apps.py 验证了meta{com.example/k: 1}与ui条目共存于最终元数据。如果允许用户直接传入ui键它会被静默覆盖所以 SDK 在装饰期就拒绝src/mcp/server/apps.py。目前无论是 TypeScript 的 ext-apps SDK 还是 FastMCP 都不会捕获上述任何一条SDK 希望你在宿主发现之前就暴露问题。超越内联 HTML用add_resource自行构建资源add_html_resource覆盖了常见场景一段 HTML 字符串。其余情况——磁盘上的 HTML 文件、程序生成的内容——需要你自己构建资源并交出去docs_src/apps/tutorial003.pyfrom pathlib import Path from mcp.server.apps import Apps from mcp.server.mcpserver import MCPServer from mcp.server.mcpserver.resources import FileResource REPORT_HTML Path(__file__).parent / report.html apps Apps() apps.tool(resource_uriui://report/app.html) def refresh_report() - str: Refresh the report data. return report refreshed apps.add_resource(FileResource(uriui://report/app.html, namereport, pathREPORT_HTML)) mcp MCPServer(report, extensions[apps])add_resource的补全与校验逻辑src/mcp/server/apps.py资源未显式指定mime_type时自动填入text/html;profilemcp-app测试 tests/server/test_apps.py显式指定了其他MIME 类型则直接拒绝任何其他 MIME 类型下的ui://资源都没有宿主会渲染错误消息MCP Apps resources are served as text/html;profilemcp-app, got text/html见 tests/server/test_apps.py资源 URI 同样必须使用ui://协议tests/server/test_apps.py。!!! tip 还在兼容读取废弃扁平键_meta[ui/resourceUri]的 GA 前宿主自己手动合并即可apps.tool(resource_uriui://x, meta{ui/resourceUri: ui://x})。嵌套的ui对象才是规范形态扁平键正在被淘汰的路上。实战运行直接体验可运行的 Apps 示例examples/stories/apps/中的appsstory 是本页内容的可运行成对实现一个带 UI 绑定时钟工具的服务端examples/stories/apps/server.py和一个完整走完协商流程的客户端examples/stories/apps/client.py。客户端会协商 Apps 支持 → 读取工具的_meta.ui.resourceUri→ 拉取 HTML 资源 → 调用工具。# stdio默认——客户端以子进程方式拉起服务端 uv run python -m stories.apps.client # HTTP——客户端在空闲端口自托管服务端运行后自动拆除 uv run python -m stories.apps.client --httpstories 的 READMEexamples/stories/apps/README.md还指出了值得关注的实现细节MCPServer(apps-example, extensions[apps])中MCPServer本身完全不知道 ui 的存在它只是应用一套封闭的扩展贡献工具 资源 能力公告apps.tool(resource_uri...)负责盖章_meta.ui.resourceUriadd_html_resource负责注册text/html;profilemcp-app资源client_supports_apps(ctx)驱动 SEP-2133 优雅降级。story 客户端还断言了能力表extensions {EXTENSION_ID: {}}与资源 MIME 类型与 tests/server/test_apps.py 中的端到端断言遥相呼应。小结MCP Apps 把工具返回数据升级为工具返回数据 宿主渲染的交互界面。在 python-sdk 中这一切收敛为Apps扩展的几个核心 APIapps.tool(resource_uri..., visibility...)绑定 UI、add_html_resource/add_resource注册text/html;profilemcp-app资源、client_supports_apps(ctx)实现优雅降级、ResourceCsp/ResourcePermissions向宿主声明 iframe 安全边界。记住四条铁律工具永远返回有意义的文本内容CSP 与权限只放资源不放工具可见性过滤交给宿主ui://协议与 MIME 类型的错误在启动期就会被 SDK 拦截。【免费下载链接】python-sdkThe official Python SDK for Model Context Protocol servers and clients项目地址: https://gitcode.com/gh_mirrors/pythonsd/python-sdk创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考