巧妙之处、边界与代码地图
把前两章的「怎么做」提炼成「为什么妙、它不做什么、和谁怎么比」,最后给一张可 grep 的跳转表。
1. 巧妙之处(可借鉴的技术)
1.1 双层 iframe + document.write 绕开 CSP base-uri
妙在把「中继逻辑」和「不可信内容」拆到两层 iframe:外层代理页是宿主控制的固定脚本,负责转发 postMessage;内层用 document.write 注入工具 HTML。改用 document.write 而非 srcdoc 是为了规避 srcdoc 与 CSP base-uri 的冲突(scripts/proxy/index.html:53-65,ProxyScript.test.ts:63-72)。
1.2 能力默认关闭(capability gating)
UI 想调工具/开链接,宿主不传对应回调就直接 MethodNotFound(AppRenderer.tsx:369-384)。不可信 UI 拿到的是一把把单独配发的钥匙,而非一串万能钥匙。配合 hostCapabilities(AppRenderer.tsx:354-358)向 UI 声明「我支持什么」,让 UI 能优雅降级。
1.3 SSRF 的多层纵深防御
externalUrl 抓取把「协议白名单 + 主机名黑名单 + 私网/链路本地 IP 段 + 超时 + 响应体大小上限」叠在一起(sdks/typescript/server/src/utils.ts:48-142)。尤其 169.254/16 那条专挡云元数据端点,是真实威胁建模的产物。
1.4 StrictMode / 异步竞态的防护
两处值得学:
cancelRef同步赋值,让 effect 清理能取消尚未 resolve 的沙箱握手 Promise(app-host-utils.ts:49-58,修了ce5893e的 StrictMode 未处理拒绝)。- 每个发送 effect 都校验
currentAppBridgeRef.current === appBridge,防止快速切换 app 时把消息发错桥(AppFrame.tsx:253/275/284)。
1.5 大 HTML 的 base64 分块
utf8ToBase64 按 8192 字节分块喂 String.fromCharCode,避免一次性展开大数组爆栈(utils.ts:222-227)。小细节,但处理几 MB 的 UI 时是必需的。
2. 边界与局限(诚实)
- 它本身不实现握手协议。
AppBridge、PostMessageTransport、JSON-RPC 握手、sendSandboxResourceReady等都在外部依赖@modelcontextprotocol/ext-apps(client 用^1.2.0,server 用^0.3.1,见各package.json)。本仓库是「符合标准的薄封装 + 渲染层 + 资源打包」。要读握手细节得去看 ext-apps。 - 客户端只有 React。
@mcp-ui/client全是 React 组件(AppRenderer/AppFrame),没有框架无关的渲染入口;Web Component 在本 commit 已不导出(见第 02 章 §7)。非 React 宿主需自行基于 ext-apps 实现。 - externalUrl 预抓取只在 TS server。 Ruby/Python 的
create_ui_resource对 externalUrl 不抓取、不注<base>、无 SSRF 防护(Pythoncore.py:131、Rubyprocess_external_url_content)。 - SSRF 不挡 DNS rebinding。
isPrivateIPv4只校验字面量 IP 字符串,域名解析到内网不在防护内 (inferred,validateExternalUrl仅检查parsed.hostname字符串)。 - 沙箱用了
allow-same-origin。 内层/代理 iframe 的 sandbox 含allow-same-origin(app-host-utils.ts:31、index.html:111)——这是document.write跨文档操作所必需,但意味着隔离强度依赖 CSP 与 origin 隔离而非纯 sandbox 属性。所以文档反复强调用「读?csp=设 HTTP 头」的代理服务器才是 tamper-proof(AppFrame.tsx:50-60)。 - 文档漂移。 README 仍描述已删除的 legacy API,容易误导。
3. 横向对比(同 shelf 兄弟)
MCP-UI 属于 ai-protocol-reference 里协议 + Web 互操作这一类。它和兄弟项目的取舍差异:
| 维度 | MCP-UI | 一般 MCP server SDK |
|---|---|---|
| 工具返回 | 可交互 UI(HTML/外链) | 文本 / 结构化 JSON |
| 信任模型 | UI 不可信,沙箱 + 能力门控 | 工具代码可信 |
| 传输 | postMessage/JSON-RPC over iframe | stdio / HTTP-SSE |
| 这层做什么 | 资源打包 + 渲染隔离 | 工具注册 + 协议握手 |
它和 MCP 本体的关系:MCP 定义工具/资源的传输,MCP-UI(=MCP Apps 标准)在其上加一层「UI 资源 + 渲染契约」。README 自述它「pioneered UI over MCP」,其模式直接影响了 MCP Apps 规范的成型。
接入点(写作时未逐一核对兄弟 doc 路径,按 shelf 约定链接):本 shelf 的 MCP 协议总览 doc、以及 任何 MCP server/client SDK 的子库 doc,都是它的近邻。
4. 代码地图(导航索引)
用符号名 grep 比行号抗漂移。
| 主题 | 文件 | 符号 |
|---|---|---|
| 打包 UI 资源(TS) | sdks/typescript/server/src/index.ts | createUIResource |
| UI → 宿主 实验性请求 | sdks/typescript/server/src/index.ts | sendExperimentalRequest |
| externalUrl 抓取 | sdks/typescript/server/src/utils.ts | fetchExternalUrl |
| SSRF 校验 | sdks/typescript/server/src/utils.ts | validateExternalUrl、isPrivateIPv4、BLOCKED_HOSTNAMES |
<base> 注入 | sdks/typescript/server/src/utils.ts | injectBaseTag |
| 元数据加前缀 | sdks/typescript/server/src/utils.ts | getAdditionalResourceProps、UI_METADATA_PREFIX |
| base64 分块编码 | sdks/typescript/server/src/utils.ts | utf8ToBase64 |
| 资源/内容类型 | sdks/typescript/server/src/types.ts | CreateUIResourceOptions、ResourceContentPayload、RESOURCE_MIME_TYPE |
| 高层渲染组件 | sdks/typescript/client/src/components/AppRenderer.tsx | AppRenderer、AppRendererProps |
| 低层 iframe 组件 | sdks/typescript/client/src/components/AppFrame.tsx | AppFrame、buildSandboxUrl、SandboxConfig |
| 沙箱握手 | sdks/typescript/client/src/utils/app-host-utils.ts | setupSandboxProxyIframe、SandboxCancelRef |
| 取/读 UI 资源 | sdks/typescript/client/src/utils/app-host-utils.ts | getToolUiResourceUri、readToolUiResourceHtml |
| 双层 iframe 代理页 | sdks/typescript/client/scripts/proxy/index.html | renderHtmlInIframe、ui-html-content、ui-proxy-iframe-ready |
| 提取 UI 元数据 | sdks/typescript/client/src/utils/metadataUtils.ts | getUIResourceMetadata |
| 判断是否 UI 资源 | sdks/typescript/client/src/utils/isUIResource.ts | isUIResource |
| 客户端能力声明 | sdks/typescript/client/src/capabilities.ts | UI_EXTENSION_CAPABILITIES、UI_EXTENSION_NAME |
| 客户端导出面 | sdks/typescript/client/src/index.ts | (看这里确认哪些 API 仍存在) |
| Ruby 打包 | sdks/ruby/lib/mcp_ui_server.rb | McpUiServer.create_ui_resource |
| Python 打包 + action 辅助 | sdks/python/server/src/mcp_ui_server/core.py | create_ui_resource、ui_action_result_tool_call |
| Python 类型 | sdks/python/server/src/mcp_ui_server/types.py | CreateUIResourceOptions、UIActionResult* |