关于 synapse
Synapse 是用于为现有 MCP 服务构建 React 界面的技能,并将界面作为 MCP Apps 资源提供。它涵盖 SDK、宿主兼容性与服务端集成流程。
主要功能
- 01将 React 界面打包为单个 HTML 文件
- 02使用 Synapse 钩子与组件库
- 03按宿主能力调整行为
- 04集成工具桥接与资源
- 05构建与验证流程
适用场景
- 01为 MCP 服务添加交互界面
- 02适配 Claude、ChatGPT 和 NimbleBrain 宿主
- 03在服务写入后更新界面数据
MCP 相关
该技能介绍如何将打包后的界面作为 MCP 资源提供,并通过 MCP Apps 宿主桥接调用现有服务端工具。它也涵盖宿主注册和能力处理。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ nimblebraininc/ skills --skill synapse安装后在 Claude Code 中输入/synapse确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/gotchas.mdhost-contract.mdportability.md详细介绍
Synapse — 为 MCP 服务构建 UI
为 MCP 服务提供交互式 UI。该 UI 是一个 React 应用,借助 @nimblebrain/synapse + Vite 构建为单个内联 HTML 文件,由服务作为 MCP 资源 ui://<name>/main 提供,并由 MCP Apps 宿主(Claude、ChatGPT、NimbleBrain)挂载在沙盒化 iframe 中,通过 postMessage 桥接通信。UI 通过该桥接调用服务的现有工具——它与数据层无关,也不需要特殊的服务框架。所有宿主使用相同的桥接;它们声明的能力各不相同,而 SDK 在缺少某项能力时会有可预测的行为。编写组件前,请先阅读下方的“可移植性”。
目标版本为 @nimblebrain/synapse@^0.20.0(已发布到 npm),以及它的对等依赖 @modelcontextprotocol/ext-apps@^1.7.5 和 @modelcontextprotocol/sdk@^1.29.0。0.x 上的插入符号范围不会跨越次版本,因此除非有人主动升级,否则应用会保持在固定版本上。在 NimbleBrain 上,0.20.0 需要宿主发布声明 message、updateModelContext 和 ai.nimblebrain/* 扩展的版本(nimblebrain#1239):在更早的宿主版本上,聊天、模型上下文、宿主操作和按键转发都不起作用,文件选择器也会拒绝操作。该软件包本身就是文档——编写代码前,请先阅读其导出的类型。相关指南位于 synapse.nimblebrain.ai。
准备工作 — 阅读 SDK 的类型(真正的文档)
从已安装的软件包(node_modules/@nimblebrain/synapse/dist/*.d.ts)或公开仓库(github.com/NimbleBrainInc/synapse)中查看:
react入口 —AppProvider及其 Hooks(useApp、useCallTool、useCallToolAsTask、useDataSync、useTheme、useHostContext、useModelContext、useSendMessage、useAction、useFileUpload、useToolResult、useToolInput、useResize)。ui入口 — 组件库(AppFrame、ListDetailLayout、ListRow、Table、Badge、Prose、ConfirmDialog、tokens,等等)。不要自行编写样式。- 类型导出 —
ToolCallResult、Theme、App接口(包括hostCapabilities)、HostCapabilityError、hostSupports、NIMBLEBRAIN_EXTENSIONS。
然后阅读 references/gotchas.md(其中介绍了不太显而易见的 API 特性,每项都能省下一轮调试)和 references/host-contract.md(NimbleBrain 清单声明 + 桥接)。请先快速浏览这些内容;它们篇幅很短,却能决定应用能否正常工作,还是陷入“为什么每次工具调用都返回 unauthenticated”的困惑。
可移植性——这是 MCP 应用,不是 NimbleBrain 应用
Claude、ChatGPT 和 NimbleBrain 都支持 MCP Apps。每个应用都会在其
ui/initialize 响应(hostCapabilities)中声明所提供的功能,每次受能力声明约束的 Synapse 调用都会在发送前检查该声明,然后在能力缺失时执行一种有文档说明的行为(resize 从不受能力声明约束,useDataSync 只负责监听):
- 有返回值的请求会以
HostCapabilityError拒绝,且不会发送——useCallTool(serverTools)、readServerResource(serverResources)、downloadFile、useCallToolAsTask(tasks)以及文件选择器; - 即发即忘调用不会发送任何内容——
useSendMessage(message)、useModelContext(updateModelContext)、useAction、forwardKeys;openLink会自行打开 URL; - 等待宿主的钩子会保留其初始值——
useToolResult/useToolInput保持为null;如果宿主不转发notifications/resources/list_changed(或你的服务器未进行声明,见第 5 步),useDataSync就不会触发。
可移植的子集包括握手、主题和宿主上下文、工具输入/结果、callTool、
sendMessage、updateModelContext 和 resize。基于这些构建的应用在所有环境中的行为都相同。
NimbleBrain 扩展恰好有三个(synapse/action、synapse/request-file、synapse/keydown),且只有在宿主于
hostCapabilities.experimental 中声明 ai.nimblebrain/<name> 时才会使用。
无操作对用户来说仍不可见,因此请读取声明并隐藏宿主无法执行的功能:
hostSupports(app, "requestFile")、app.hostCapabilities.message、app.supportsTasks。
<AppProvider> 会在握手完成前不渲染任何内容,因此所有这些状态都会在组件读取之前确定——如果握手始终没有返回(直接打开构建后的 HTML),则 Provider 下的内容也完全不会渲染。
各钩子的表格、扩展列表、向 Claude 和 ChatGPT 提供同一个应用,以及两个连接入口:references/portability.md。
流程
-
分析服务器——语言(Python/FastMCP 或 TS)、传输方式(stdio 与 HTTP-native/edge-fronted)、工具列表及返回形状,以及部署形式(
.mcpb包还是容器映像——容器需要 Node 构建阶段,见第 7 步)。 -
搭建
ui/——package.json(react/react-dom^19、@nimblebrain/synapse@^0.20.0、@modelcontextprotocol/ext-apps@^1.7.5、@modelcontextprotocol/sdk@^1.29.0、vite、vite-plugin-singlefile、typescript;仅在渲染 markdown 时添加marked和dompurify)、vite.config.ts(react()、viteSingleFile()、synapseVite()、build.assetsInlineLimit: Infinity)、严格的tsconfig.json、index.html和.gitignore(node_modules/、dist/、.vite/)。提交package-lock.json,以便构建时运行npm ci。 -
构建
App.tsx——<AppProvider name="<server>" version="<version>">。在 Vite 入口文件(main.tsx)中导入一个副作用模块:import "@nimblebrain/synapse/ui/base"(AppFrame填充所需的根高度链——在首次绘制前应用;注意事项 M)。不要导入字体——SDK 不附带字体,和其他主题值一样,字体排版由宿主提供(注意事项 N)。使用包提供的AppFrame外壳(通过AppFrame.Body bleed承载ListDetailLayout/SidebarLayout)——绝不要手动设置height:100vh(注意事项 D)。使用useCallTool(name)或围绕useApp().callTool(name, args)封装的轻量useCall<T>()来驱动数据;使用useDataSync(() => …)重新读取数据,该钩子会在服务器声明写入操作时触发(第 5 步)——回调不会指定任何工具,因此始终应重新读取;使用tokens/useTheme设置主题;使用useModelContext推送 Agent 上下文。主列表使用ListRow,不要使用Table(注意事项 D——Table会超出固定宽度的侧边栏,并覆盖详情窗格)。 -
清理所有渲染的 HTML——务必执行(存储型 XSS)。 如果通过
Prose/dangerouslySetInnerHTML渲染由服务端或 Agent 编写的 Markdown(备注、描述、研究输出),请先使用 DOMPurify 处理:DOMPurify.sanitize(marked.parse(md, { async: false }) as string)。iframe 以allow-scripts运行,并设置了script-src 'unsafe-inline'CSP,因此,真正阻止注入的<script>/onerror以完整的工具桥接权限运行(读取/外传/修改工具可访问的一切内容)的只有清理操作,而不是 CSP。纯粹使用<Text>{value}</Text>是安全的(React 会转义内容)。完整攻击链:gotcha K。 -
将 UI 作为资源提供,并在写入后发出通知。 使用
@mcp.resource("ui://<name>/main")返回构建后的ui/dist/index.html,并以规范规定的 MIME 类型text/html;profile=mcp-app提供服务(fastmcp3.4+ 中,ui://URI 默认使用此类型;其他情况请显式传入mime_type)。通过环境变量(<APP>_UI_DIR)解析路径,并以相对于__file__的路径作为备用方案(gotcha E——安装后的包位于 site-packages 中,因此相对于__file__查找ui/dist会找不到文件)。将其设为纯文件读取:不涉及数据库/身份验证会话,不包含身份信息的 HTML,所有租户数据都在运行时通过桥接获取。然后,让每个执行写入的工具在写入提交后发送notifications/resources/list_changed——这是useDataSync唯一会响应的通知,也能覆盖所有写入方(Agent、其他视图、Webhook):from fastmcp import Context from mcp.types import ResourceListChangedNotification @mcp.tool() async def save_item(item: Item, ctx: Context) -> dict: stored = store.save(item) await ctx.session.send_notification(ResourceListChangedNotification(), related_request_id=ctx.request_id) return storedrelated_request_id会让通知通过 HTTP 随所属调用的响应返回。每次写入通知一次,而不是每行通知一次。完整指南:synapse.nimblebrain.ai/docs/guides/keep-ui-in-sync/。 -
向宿主注册应用。 第 2–5 步生成的 bundle 无论如何都相同;区别仅在于注册方式。面向 NimbleBrain 宿主——在
manifest.json中添加_meta["ai.nimblebrain/host"]:添加一个placements[]条目(slot: "sidebar.apps"、resourceUri: "ui://<name>/main"、route、label、icon)。完整规范与选项:references/host-contract.md。面向 Claude 或 ChatGPT——宿主会渲染工具指定的资源:使用工具上的_meta.ui.resourceUri绑定资源,以text/html;profile=mcp-app提供该资源(不要再以其他 MIME 类型提供第二份副本),声明ui.visibility和ui.csp,并且——如果服务端需要登录——配置securitySchemes和401挑战。Python 服务端可通过nimblebrain-synapse的SynapseUI完成所有这些配置。然后运行npx @nimblebrain/synapse check --target claude,chatgpt <server-url>。详情:references/portability.md。 -
以容器部署的服务端——多阶段构建。
.mcpbbundle 会从发布 CI 获取ui/dist;容器镜像必须自行构建:在node:22构建阶段运行npm ci && npm run build,然后在运行时阶段执行COPY --from=builder …/ui/dist(确保运行时不依赖 Node),并设置 UI 目录环境变量。从运行时层移除ui/源码——只打包dist。 -
本地预览——运行
cd ui && npm run dev→ 打开/__preview。对于由边缘服务代理的服务端(从 HTTP 标头获取身份信息,并使用数据库),无法通过 stdio 预览驱动真实服务端,因此每次调用都会返回unauthenticated——通过环境标志将synapseVite({ serverCmd })指向一个已填充数据的 stdio 模拟服务端(gotcha F)。 -
验证——运行
npx tsc --noEmit和npm run build(Vite/esbuild 本身不会进行类型检查)。然后运行服务端项目自身的 lint/格式化/测试检查流程(例如make verify、ruff format --check),而不只是 UI 类型检查——即使类型检查通过,仅格式化差异也会导致 CI 失败。在浅色和深色模式间切换预览,并逐一检查所有界面——不考虑主题的颜色虽然能通过tsc/build,却可能只在一种模式下出错。所有tokens.*在两种模式下都有定义;自定义变量则没有(gotcha L)。确认服务端以text/html;profile=mcp-app提供ui://<name>/main,并确认通过预览执行写入后视图会刷新(预览会转发服务端的list_changed,gotcha F)。
不在范围内
- 宿主侧渲染 — 宿主已实现放置 → iframe → 桥接;你只需构建包并声明放置,无需做其他工作。
- 声明式宿主“命令”/工具栏 — 它们并不存在。应用内按钮位于 iframe 中,并通过
useCallTool调用工具。