mcpField
S
synapse

synapse

github.com/nimblebraininc开发340 次安装2 周前更新
代码仓库

关于 synapse

Synapse 是用于为现有 MCP 服务构建 React 界面的技能,并将界面作为 MCP Apps 资源提供。它涵盖 SDK、宿主兼容性与服务端集成流程。

主要功能

  1. 01将 React 界面打包为单个 HTML 文件
  2. 02使用 Synapse 钩子与组件库
  3. 03按宿主能力调整行为
  4. 04集成工具桥接与资源
  5. 05构建与验证流程

适用场景

  1. 01为 MCP 服务添加交互界面
  2. 02适配 Claude、ChatGPT 和 NimbleBrain 宿主
  3. 03在服务写入后更新界面数据

MCP 相关

该技能介绍如何将打包后的界面作为 MCP 资源提供,并通过 MCP Apps 宿主桥接调用现有服务端工具。它也涵盖宿主注册和能力处理。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/nimblebraininc/skills --skill synapse

安装后在 Claude Code 中输入/synapse确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
gotchas.md
host-contract.md
portability.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。

流程

  1. 分析服务器——语言(Python/FastMCP 或 TS)、传输方式(stdio 与 HTTP-native/edge-fronted)、工具列表及返回形状,以及部署形式(.mcpb 包还是容器映像——容器需要 Node 构建阶段,见第 7 步)。

  2. 搭建 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。

  3. 构建 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 会超出固定宽度的侧边栏,并覆盖详情窗格)。

  4. 清理所有渲染的 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。

  5. 将 UI 作为资源提供,并在写入后发出通知。 使用 @mcp.resource("ui://<name>/main") 返回构建后的 ui/dist/index.html,并以规范规定的 MIME 类型 text/html;profile=mcp-app 提供服务(fastmcp 3.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 stored
    

    related_request_id 会让通知通过 HTTP 随所属调用的响应返回。每次写入通知一次,而不是每行通知一次。完整指南:synapse.nimblebrain.ai/docs/guides/keep-ui-in-sync/。

  6. 向宿主注册应用。 第 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。

  7. 以容器部署的服务端——多阶段构建。 .mcpb bundle 会从发布 CI 获取 ui/dist;容器镜像必须自行构建:在 node:22 构建阶段运行 npm ci && npm run build,然后在运行时阶段执行 COPY --from=builder …/ui/dist(确保运行时不依赖 Node),并设置 UI 目录环境变量。从运行时层移除 ui/ 源码——只打包 dist。

  8. 本地预览——运行 cd ui && npm run dev → 打开 /__preview。对于由边缘服务代理的服务端(从 HTTP 标头获取身份信息,并使用数据库),无法通过 stdio 预览驱动真实服务端,因此每次调用都会返回 unauthenticated——通过环境标志将 synapseVite({ serverCmd }) 指向一个已填充数据的 stdio 模拟服务端(gotcha F)。

  9. 验证——运行 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 调用工具。

评论

支持 Markdown