WorkBuddy 的连接器是怎么统一到 MCP 的

3 阅读8分钟

用 WorkBuddy 时你可能做过三件事:让它读 Gmail 邮箱、把一个站点部署到云端、或者自己在 mcp.json 里填一个本地服务。界面上这三件事都叫连接器,模型看到的也都长成 mcp__server__tool。实现上它们走三条完全不同的路:桌面端内置的能力总线、第三方连接器包、你自己写的 MCP server。

WorkBuddy 的做法是统一命名,但凭据怎么存、调用怎么审批,三层各有一套。

这篇按一个连接器在系统里的生命周期走:注册进来 → 拿到调用资格 → 暴露成工具 → 一次调用经过的检查 → 撤销与失效。每个环节都看三层各自的答案。

注册:连接器以什么形式存在#

三条路的进程来源各不相同:内置能力总线跑在桌面主进程里,第三方连接器包自带一个 MCP server,用户自定义 MCP 由用户或插件启动。

第一层不用安装。桌面主进程起一个本地 MCP 主机,服务名 connector-proxy,把平台能力注册成 MCP 工具,路由挂在 /mcp:云服务连接、站点部署、插件市场、技能安装、会话搜索都在上面,比如 connect_cloud_service、workbuddy_sites_deploy、search_and_install_skills、conversation_search、search_plugins。同样用本地主机挂出来的还有 miora(设计、图像、视频)、sheetagent(表格)、weixinpay、ardot、netdrive。

第二层是第三方连接器包,Gmail、日历、网盘、企业系统都走这条路。包本身是一个标准目录,以 Gmail 为例:

gmail-mcp/
├── connector.json      ← 元数据:名称、描述、示例、type: "mcp"、source
├── mcp.json            ← 标准 MCP 配置(stdio 或 http)
├── oauth-client.json   ← OAuth 客户端配置
├── dist/               ← 连接器实现(如 cli.cjs)
├── icon.svg
└── THIRD_PARTY_NOTICES.md

connector.json 管展示和搜索,mcp.json 管怎么启动。安装后,连接器在 MCP 管理器里以 connector:<id> 注册。

第三层是配置文件:~/.workbuddy/mcp.json,标准 mcpServers 结构;插件自带的 .codebuddy-plugin/.mcp.json 在安装或启用时同样被纳入 MCP 管理器。

连接器也不一定是一个本地包。连接器市场和云端 Agent 的连接器走后端 API(/v2/user/cloudagent/agents/{agentId}/connectors 的增删改查),同一个概念在本地桌面和云端两个运行时里保持一套语义。

调用资格:三层各管一套凭据#

第一层防的是「本机以外的人」。服务只绑回环地址,请求还要校验 Host 的主机名在回环集合内、端口和监听端口一致,否则 403。外部机器连不上;浏览器把恶意域名解析到 127.0.0.1 时,Host 头仍是攻击者的域名,同样被拒。Origin 再过一遍白名单:没有 Origin 的请求(本机 CLI 和脚本)放行,带浏览器 Origin 的必须在白名单里。

资格本身按路由签发。McpAccessTokens.issue(routeKey) 生成 32 字节随机数、base64url 编码,路由和令牌双向映射;令牌只在该路由注册期间有效。

只有 Bearer token 还不够。请求得再带一个 HMAC-SHA256 签名的上下文信封,签名密钥至少 32 字节,信封里绑定 sessionId、projectId、workspace、welcomeMode。校验通过后,处理函数拿到的不是「某个客户端」,而是「某个会话」,后面所有权限判断都按这个粒度做。

第二层的关键是长期凭据不进连接器进程。Gmail 的 mcp.json 里那几个环境变量就是全部:

{
  "mcpServers": {
    "gmail": {
      "type": "stdio",
      "command": "node",
      "args": ["${WORKBUDDY_GMAIL_CONNECTOR_ROOT}/dist/cli.cjs"],
      "env": {
        "WORKBUDDY_GMAIL_API_URL": "${WORKBUDDY_LOCAL_MCP_ORIGIN}/mcp/internal/gmail/api",
        "WORKBUDDY_GMAIL_CREDENTIAL_AUTHORIZATION": "${WORKBUDDY_LOCAL_MCP_AUTHORIZATION}",
        "WORKBUDDY_MCP_CONTEXT": "${WORKBUDDY_MCP_CONTEXT}",
        "CODEBUDDY_PROJECT_DIR": "${CODEBUDDY_PROJECT_DIR}"
      }
    }
  }
}

连接器进程拿到的只有短期授权、会话上下文,以及指向本地 MCP 主机的内部 API 地址。真正的 Google OAuth 凭据留在桌面端,连接器只把 MCP 调用翻译成对内部 API 的请求。连接器实现有漏洞,攻击者也拿不到长期 token。

OAuth 本身由 McpManager 统一管生命周期:beginAuthorization(id) 生成授权 URL 并监听本地回调,客户端状态在 starting / ready / unauthorized / error 之间迁移,回调结果交给 onDidOAuthReconnectSettled,成功走 completeMcpConnect(id),失败或未授权走 failMcpConnect(id, state, error)。

第三层没有授权服务器,改用本地信任库 ~/.workbuddy/mcp-approvals.json,键是服务器名加配置哈希:

function approvalKey(config, serverName) {
  return `${serverName}::${hash(config)}`;
}

用户批准过 my-server,不等于批准过「my-server 被改成执行另一段命令」。配置内容一变哈希就变,旧批准自动失效,要重新确认。写入路径是先校验、再提交、失败回滚;读取失败时沿用上一份快照,而不是把读不到当成没有配置。插件如果声明了高权限工具,还要另外走审批闸门。

暴露:都变成模型面前的同一个工具#

三层各说各话到这里汇合。命名统一成 mcp__<server>__<tool>,比如 mcp__connector-proxy__connect_cloud_service,名字自带归属,排障时直接看出工具来自哪个服务。connector-proxy 还支持裸名和别名展开(expandMcpAppToolNameAliases),为每个工具生成 tool_control_id 和兼容 ID,UI 的工具开关、灰度、兼容旧配置都依赖这层映射。

工具列表不是全局配置,是按会话算出来的。内置总线的每个会话上下文都带一个 allowedToolNames,加上工具自身的可用性判断,同一个账号在不同会话、不同项目、不同模式下看到的工具面可以不一样。

几十个连接器、上百个工具没有全量塞进上下文,靠的是 defer_loading:标记为延迟加载的工具不进首屏工具目录,要用时通过 ToolSearch 按需发现,系统提示词不用一开始背完整目录。

CLI 侧拿到的是一份标准 MCP 配置:

{
  type: "http",
  url: `http://127.0.0.1:${port}/mcp`,
  headers: {
    Authorization: `Bearer ${token}`,
    [CONTEXT_HEADER]: signedEnvelope,
  },
  transparent_proxy: true,
  builtin_tools: [...],
  tools: { /* defer_loading、prompt_cache_stability、tool_control_id */ }
}

transparent_proxy: true 表示这个 MCP 服务由桌面本地主机透明代理,CLI 不再为它起子进程。这既是性能优化,也是边界:进程的生命周期和凭据都留在桌面端,宿主掌握系统能力和登录态,CLI 只按 MCP 协议调用。

一次 tools/call 经过哪些检查#

把前面几段串起来,一次对内置总线工具的调用依次经过:

  1. 回环校验:Host 主机名与端口、Origin 白名单;
  2. Bearer 令牌:必须是这条路由当前注册期间签发的那个;
  3. 信封验签:HMAC 通过才解出会话上下文;
  4. 工具存在、会话允许、当前上下文可用,三个条件同时满足才执行;
  5. 计费与高危工具再过一道 PreToolUse 审批钩子;
  6. 执行。第三方连接器的调用落在连接器进程里,它再翻译成一次带短期授权和上下文的内部 API 请求。

第 4 步的判断只有三行:

const tool = registry.get(params.name);
const ctx = toSessionToolContext(session);

if (!tool || !session.allowedToolNames.has(tool.name) || tool.isAvailable?.(ctx) === false) {
  return errorResult(`Tool "${params.name}" is unavailable in this session`);
}

计费工具另有一道闸门#

图像生成、视频生成、图像处理、音频转写这类高积分工具,每次调用都实际扣费,不能和普通查询共用一套审批策略。

high-credit-gate.cjs 是一个 PreToolUse 钩子,扫描各市场的 high-credit-tools.json,命中清单就返回 permissionDecision: "ask" 强制弹审批。判定发生在所有权限规则和 bypassPermissions 之前,所以默认 bypass 模式也绕不过去。审批文案、审批记忆分组、hook matcher 都从同一份清单派生,新增计费工具只改 JSON,不改代码。

{
  "version": 1,
  "tools": {
    "miora_text_to_image":  { "approvalKind": "imageGen", "group": "miora-image" },
    "miora_text_to_video":  { "approvalKind": "videoGen", "group": "miora-video" },
    "miora_edit_image":     { "approvalKind": "imageGen", "group": "miora-image" }
  }
}

清单里有两处设计是刻意的。一是图片和视频分开分组:用户点「本次会话始终允许生成图片」,不应该顺带授权更贵的视频生成。二是 fail-closed:清单损坏、声明非法、脚本异常,一律按「需要审批」处理,因为上游在 hook 抛错时会 fall-through 到常规权限检查,在 bypass 下等于静默放行、零审批扣费,所以宁可多弹一次窗。

还有个前提藏在加载路径里:计费工具的插件必须在 .mcp.json 里显式写 defer_loading: false。走延迟加载的话,调用会先经过 DeferExecuteTool,它在 bypass 模式下的前置检查直接返回「无需审批」,钩子根本不执行。安全机制依赖加载路径,这是个很典型的坑。

撤销与失效#

调用资格不是签了就长期有效,三层各有各的失效路径。

内置总线的路由注销时,对应令牌立即撤销,并中止这条路由上所有在飞请求。

连接器要等 OAuth 结算。流程里维护一个 pending 集合,OAuth 没结算的连接器不算可用,只有 resolveMcpDependency 返回 ready 才调用 completeMcpConnect。解绑走 revokeCredentials(id, { remote: true }),要求远端授权也撤销成功,避免出现本地删了、云端还授权的残留。

用户 MCP 的批准跟着配置内容走,改一行命令就要重新确认。

撤销要盯这么多处,是因为一个连接器的状态分散在安装态、启用态、OAuth 态、MCP 客户端态和会话可见性之间,一致性得专门维护。

和 MCP 规范差在哪#

MCP 规范里的授权模型,核心是把 MCP server 当成 OAuth 2.1 的资源服务器:客户端从授权服务器拿 token,访问 token 由资源服务器校验,支持受保护资源元数据、动态客户端注册、PKCE。

WorkBuddy 在它上面加了一层本地授权代理,三层各取所需:第三方的 OAuth 仍按标准流程走,但凭据集中在桌面端,连接器进程只拿短期本地授权;本地能力不暴露公网 OAuth,用回环、路由令牌、上下文签名构造一条只对当前会话有效的调用通道;用户 MCP 用配置哈希审批替代 OAuth,因为很多本地 MCP server 根本没有授权服务器。

三套机制解决三个不同的问题:第三方授权、本地能力隔离、用户配置信任。产品层把它们都叫连接器,实现层各自有独立的信任模型。

好处是模型侧简单、凭据侧收缩:模型不需要理解工具是内置还是第三方,长期 token 不用下发给连接器进程,异常时各环节都倾向要求确认而不是默认放行。代价主要落在调用链上:一次调用要经过路由解析、令牌校验、信封校验、会话白名单、可用性判断、可能的审批钩子,任何一环出问题排障成本都不低;defer_loading 约束、审批分组、清单格式都是跨进程契约,改任何一处都可能让安全机制静默失效。

剩下的边界#

上面的机制各自还挂着一个前提:会话白名单依赖会话初始化正确;连接器只拿短期授权,靠的是本地内部 API 自身鉴权可靠;高积分闸门依赖清单完整、且插件走非延迟加载;解绑依赖远端撤销接口可用。这些前提之外,还有四类风险是这套设计挡不住的。

同用户的本机进程仍然能读配置、拿令牌。回环加令牌防的是其他网页和未授权客户端,不是同一用户下的恶意程序,桌面应用的本地信任边界最终还是依赖操作系统账户隔离。

oauth-client.json 里可能带 client_secret。桌面应用内置 OAuth 客户端密钥在业界不罕见,但它应被视为公开信息,不能当真正的秘密;更稳妥的做法是用 PKCE 的公共客户端模式,不内置 secret。

工具输出是新的注入面。连接器返回的邮件、文档、网页内容都可能包含提示词注入,MCP 协议本身不解决这个问题,得在工具描述、输出包装和审批策略上一起处理。

还有审批疲劳:高积分工具每次都问,用户会习惯性点允许,分组记忆和文案分级只能缓解,不能消除。

最后#

连接器这个词容易让人以为只是多接几个 API。真正难的是让三种来源在模型面前长得一样,同时在系统内部保持三种信任模型。本地能力靠「回环 + 每路由令牌 + 上下文签名」,第三方凭据集中在桌面端,用户配置审批绑定内容哈希——这三件事各自独立,但得一开始就定下来。

计费和高危工具的闸门还要单独验证:它在延迟加载和 bypass 模式下是否同样生效。机制写在文档里,不等于它在每条路径上都跑得到。

文中的机制、默认值和字段按 WorkBuddy 5.8.4 整理。没有做真实 OAuth 授权、令牌窃取或本地端口扫描,所以文中「能防住什么」是机制层面的判断,不是渗透测试结论。为避免扩散敏感信息,不转载任何 OAuth 客户端配置的具体值。