开源项目 OpenWork 不装桌面应用也能用:两个 MCP 工具接进 Agent

2026-08-04

本文基于 openwork 仓库 commit 3b41381(2026-08-03)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/different-ai/openwork 最新代码与文档为准。

**OpenWork 这个开源桌面应用最值得单独拿出来看的,不是那个 Electron 壳,而是它给外部 Agent 留的那道门:整个连接只注册两个 MCP 工具,一个叫 search_capabilities,一个叫 execute_capability,其余所有能力都作为数据出现在搜索结果里,而不是作为工具定义挤进你的上下文。**你不装桌面应用,照样能把这道门接到手头的 Claude Code、Codex 或 OpenCode 上。

先把命名说清楚:这里的 OpenWork 是 GitHub 上 different-ai/openwork 这个开源桌面应用项目,跟同名的职场点评网站、跟中文里泛指的「开放工作」没有任何关系。下文出现 OpenWork 三个字,都是指这个仓库。

本站已经写过 MCP 协议本身是什么本机装了一堆 MCP Server 之后怎么收拾去哪里找可信的 Server,这三篇管的是协议层和你自己的机器;这篇不重复它们,只盯 OpenWork 用两个工具当门面这一个具体设计,看它把复杂度挪到了哪儿、代价是什么。

一、它想解决的是「工具定义把上下文吃光」

任何一个接过五六个 MCP Server 的人都碰到过同一件事:每个 Server 都要在会话初始化时把自己的工具列表塞进模型上下文,工具一多,光是工具定义就能吃掉可观的一块预算,而且模型还得在几十上百个名字里挑。这个问题的常见解法是手工裁剪——关掉不常用的 Server、给工具改名、按项目切配置,本质上是让人替模型做减法,具体做法可以看 MCP 工具数量该怎么控

OpenWork 走的是另一条路:不给你一张工具清单,只给你一个搜索入口。仓库文档里对这个取舍的表述很直白,packages/docs/model-context-protocol/claude-code.mdx 里写的是,服务端只暴露 search_capabilitiesexecute_capability,所以客户端是「搜索你能访问的东西、然后按精确能力名执行」,而不是把上百个工具定义装进上下文。同一段话在 codex.mdxopencode.mdx 里几乎原样重复,区别只是把句中的客户端名字换成对应的那一个,说明这是项目刻意反复强调的定位,不是某一篇文档的随口一说。

代码里也确实是这么写死的。ee/apps/den-api/src/mcp/agent.ts 里给模型的连接说明第一句就是:这个连接刻意只暴露两个工具;第四句是:先用 2 到 4 个关键词变体调 search_capabilities,再用它返回的精确名字调 execute_capability,不要凭空猜名字。docs/marketplace-capabilities-architecture.md 里在讨论后续扩展时还专门立了一条不可动摇的约束——就算再接进新的能力来源,「MCP 工具面不会变大,这条通道仍然只暴露两个工具」,并且有一份回归流程 evals/flows/mcp-search-capabilities.flow.mjs 专门断言工具列表恰好是这两个。

这个设计的代价随之而来:模型看不见清单,就必须先会搜。所以 OpenWork 把大量本来写在工具描述里的引导,改写进了连接级说明和搜索结果的字段里。关于工具描述这层文字有多要命,可以对照 Agent 工具描述该怎么写 一起看。

二、两个工具的真实形状

搜索工具的入参在 ee/apps/den-api/src/mcp/agent.ts 里定义,裁掉冗长的 describe 之后是这样:

inputSchema: z.object({
  query: z.string().min(1),
  limit: z.number().int().min(1).max(20).optional(),
  type: searchCapabilityTypeSchema.optional(),
}),

limit 不传时默认 5,上限被 zod 卡在 20。type 是来源过滤器,枚举定义在 ee/apps/den-api/src/mcp/search.ts

export type SearchCapabilityType = "all" | "api" | "admin" | "mcp" | "marketplace" | "skills"

这个枚举本身就是一张能力来源地图:api 是 Den 的 REST 目录(含原生的 Google Workspace 能力路由),admin 是只对允许名单内的平台管理员可见的命名空间工具,mcp 是组织侧接进来的外部 MCP 连接,marketplace 是市场插件里的能力,skills 是内置与市场技能。搜索时这几路分别取候选,合并后按 compareCapabilityMatches 排序再截断到 limit

排序规则没有任何玄学。search.ts 里的 scoreText 就是纯粹的词元打分:查询词命中名字词元加 5 分,前缀互相匹配加 3 分,命中摘要加 2 分,命中额外词元(比如路径)加 1 分。名字还会先被 tokenizeToolName 按驼峰拆开,所以查 organization 能命中 getOrganizationscompareCapabilityMatches 在分数之上还有一条优先级:kindconnection_status 的匹配项永远排在最前——也就是说,当某个连接需要人去点一下才能用时,这条提示会插到结果最上面,而不是被淹没在得分里。

执行工具的入参同样在 agent.ts

inputSchema: z.object({
  name: z.string().min(1),
  schemaDigest: z.string().regex(/^sha256:[a-f0-9]{64}$/).optional(),
  path: z.union([z.record(z.string(), z.unknown()), z.string()]).optional(),
  query: z.union([z.record(z.string(), z.unknown()), z.string()]).optional(),
  body: z.unknown().optional(),
}),

关键在于模型怎么知道该填哪几个。答案是搜索结果自带形状。search.ts 里的 CapabilityMatch 类型带着 pathParamsqueryParamshasBody,需要 JSON 体的还会带 bodySchema;如果这条匹配来自外部 MCP,则会带上提供方公布的 argumentsSchemaschemaDigest 以及 invocation.argumentsField。文件顶部的注释把意图写得很清楚:在 /mcp/agent 这个精简端点上,匹配结果是唯一的发现途径,所以每条匹配必须携带足够的形状信息,让调用方不用猜就能拼出一次合法的 execute_capability

错误也是结构化的,不是一句自由文本。名字对不上返回 unknown_capability,消息里直接指示重新搜索;外部提供方判定参数不合法返回 invalid_capability_arguments,并附上 issues 列表;超时返回 capability_timeoutagent.ts 的连接说明里配了对应的行为约束:拿到 invalid_capability_arguments 要改参数再重试一次,绝不允许原样重试;拿到 unknown_capability 要先重新搜索。还有一条更细的:本地 schema 校验发现不一致时,OpenWork 依然会把调用发给下游提供方,把不一致作为 schemaGuidance 附在结果旁边——提供方要是成功了就接受结果,别因为一句告警就重试。

三、能力是从哪几路汇进来的

搜索背后有四到五个来源,各自的实现文件是分开的。下面这张表里的路径都是仓库里真实存在的位置。

组成部分它负责什么对应仓库位置你什么时候会碰到它
两工具门面注册 search_capabilitiesexecute_capability,合并各来源结果,写连接级说明ee/apps/den-api/src/mcp/agent.ts想知道模型到底看到了什么提示时
搜索与打分词元化、打分、排序、CapabilityMatch 类型ee/apps/den-api/src/mcp/search.ts搜不到东西、想搞清关键词该怎么给时
REST 目录从 OpenAPI 生成能力目录ee/apps/den-api/src/mcp/catalog.ts排查某个后台操作为什么没出现时
暴露策略按标签、路径、operationId 过滤掉不该给 Agent 的路由ee/apps/den-api/src/mcp/policy.ts 与同目录 README.md确认哪些操作被有意挡掉时
外部 MCP 汇入把组织加的 Notion / Linear 之类连接的工具并进同一条通道ee/apps/den-api/src/mcp/external-capabilities.ts某个第三方工具搜得到却调不通时
市场能力让市场插件里的技能、命令、上下文可搜可执行ee/apps/den-api/src/mcp/marketplace-capabilities.ts团队发布的技能没人装却想直接用时
客户端接入指南十种客户端的配置与登录步骤、支持状态表packages/docs/model-context-protocol/(10 份 mdx)第一次接入或换客户端时
分发方式三种打包分发路径packaging/(aur、docker、helm 三个目录)要自己部署而不是下载安装包时

来源之间的差别值得留意。市场能力那一路是纯数据库读取,不发网络请求;外部 MCP 那一路则可能真的去调远端的 tools/list,所以搜索延迟和失败模式完全不同。docs/marketplace-capabilities-architecture.md 把这个区别写在了明处,也说明了命名空间怎么区分:外部连接的能力名形如 mcp:<connectionId>:<toolName>,市场能力形如 plugin:<pluginId>:<configObjectId>。这些名字是搜索结果里的数据,不是注册的 MCP 工具名,所以不受工具名长度限制约束。

搜索能看到什么,还受一层权限约束。ee/apps/den-api/src/mcp/README.md 列了允许暴露的标签(Members、Teams、Roles、Plugins、Marketplaces、Workers 等),也列了被有意挡住的:AdminAuthenticationSystemWebhooks 四类标签整体排除,路径以 /api/auth 开头、含 /admin、含 /webhooks 的一律拦掉,另外还逐个点名了几个即使标签放行也必须挡住的操作——创建和删除 API Key、铸造 worker token、断开 OAuth 提供方,理由是这些要么返回凭据、要么是不该由 Agent 代劳的破坏性变更。cloud-mcp.mdx 用一句话概括了同一件事:它有意不暴露认证内部、管理员专用系统路由、webhook、API Key 的创建与删除,以及任何会返回凭据的端点。

四、接到你现有的 Agent 上

三种客户端的配置在仓库文档里都是现成的。Claude Code:

claude mcp add --transport http openwork https://api.openworklabs.com/mcp/agent

-s user 可以让它在每个项目里都可用。加完之后在 Claude Code 里运行 /mcp,选中 openwork,走完浏览器登录并选择组织。

Codex 分两步,先加再登录:

codex mcp add openwork --url https://api.openworklabs.com/mcp/agent
codex mcp login openwork

OpenCode 是往 opencode.json 里加一段配置,再执行 opencode mcp auth openwork

{
  "mcp": {
    "openwork": {
      "type": "remote",
      "enabled": true,
      "url": "https://api.openworklabs.com/mcp/agent",
      "oauth": {}
    }
  }
}

三份文档在语气上有个差别值得注意:opencode.mdx 的提示框写的是 OpenCode 是「已验证」的客户端,原生远程 MCP OAuth 通过了端到端实现测试;而 claude-code.mdxcodex.mdx 的提示框都明说这只是「配置指南」,被标为 setup only 是因为原生验证尚未完成。packages/docs/cloud/run-in-the-cloud/cloud-mcp.mdx 里那张客户端支持状态表把十种客户端逐一列了状态,只有 OpenCode 是 Verified,其余都是 Setup only。你选客户端时,这张表比任何宣传语都有用。

协议侧的要求也写得很细:这是一个远程 Streamable HTTP MCP 服务器,受保护资源就是那个 /mcp/agent 地址;客户端用 RFC9728 做受保护资源发现;授权与浏览器登录源是另一个域;授权和取令牌请求必须带且只带一个 resource 值;公共客户端强制 PKCE,且只支持 S256;重定向 URI 必须是 HTTPS 回调或 http://127.0.0.1:<port>/callback 这类环回回调,私有协议回调原则上不接受,只对个别已知原生客户端开了精确白名单。你的客户端如果不支持远程 MCP 的 OAuth,文档直说:接不上。

还有一个容易忽略的语义:你在浏览器里选的那个组织,会被钉进令牌。之后你在 OpenWork 里切换当前组织,并不会改变外部客户端手上那个令牌指向的组织。要换组织,只能按各客户端的登出再登录流程走一遍,codex mcp logout / opencode mcp logout 或者在 Claude Code 的 /mcp 里清掉重认证。

五、边界与代价:它明确不管的事

这个设计不是白拿的,代价相当具体。

你放弃了工具清单带来的可预测性。 模型不再有一份静态列表,能不能用上某个能力,取决于它搜没搜对关键词。打分逻辑是纯词元匹配,没有语义检索,同义词不会自动命中——所以连接说明里才要反复叮嘱试 2 到 4 个关键词变体。这也意味着你很难在会话开始前静态地确认「这次任务需要的能力都在」。

这条通道依赖一个远端服务。 它不是本地进程,是托管在网络另一头的端点,需要账号、需要组织、需要客户端支持 OAuth 浏览器登录。断网、令牌过期、组织成员资格被撤销,都会让能力整体消失。文档里写明访问会持续对照活跃会话与组织成员资格复核,移除成员或吊销会话就等于切断 MCP 访问——这对管理员是特性,对个人用户是单点。

有些类型它承认自己执行不了。 docs/marketplace-capabilities-architecture.md 里那张能力类别表把话说得很老实:hook 类只可搜到元数据,执行时返回一条不支持的提示,理由是 apps/server/src/claude-plugin-bundle.ts 会警告 OpenWork 不支持 hooks,本地安装也会跳过加载;tool 类返回 status: "needs_install" 和一条指明插件与市场来源的提示,也就是说仍然需要人或桌面安装流程去本地完成。还有 content_not_synced 这种降级态,表示配置对象存在但内容还没同步过来。这些状态是设计出来的诚实信号,不是 bug,但你规划工作流时得把它们算进去。

市场里的指令性内容是提示注入面。 这一点仓库自己在安全小节里写了:市场内容虽由组织策划,但仍是第三方文本,指令性载荷构成提示注入面;缓解手段是每份载荷都带来源框注、严格的授权、以及桌面端既有的「Agent 不自动打开工具输出里的 URL」这条约束。换句话说,风险被承认并被限制,而不是被消除。你把这类通道接进能改代码、能发邮件的 Agent 时,权限边界要自己再收一道,思路可以参考 最小权限怎么设计

凭据与授权范围要看清楚。 这类产品的本质是替你集中保管对第三方服务的授权。文档里描述的能力覆盖 Gmail 的读取与搜索、日历的列出与创建、云盘的搜索与读取、Gmail 草稿创建,以及组织接入的各种外部 MCP 连接。docs/external-mcp-oauth.md 说明了外部连接的令牌、刷新令牌、客户端密钥、PKCE 验证串和待处理的授权事务都加密留在服务端,不进 Agent 引擎——这是个合理的设计,但换个角度看就是:这些授权集中在一个地方,那个地方被攻破的后果也是集中的。接之前请自己盘一遍:授权给了哪些范围、数据流经哪里、谁能撤销。

企业侧能看到和能管到的东西不少。 README 里描述的 OpenWork Den 是团队与组织的控制面:按成员和团队控制能用哪个模型提供方、设置桌面策略、限制本地模型访问、控制组织可以使用哪些应用版本、通过市场发布技能与插件并分配到组织/团队/个人。docs/desktop-app-policies.md 进一步说明桌面策略是从云端 GET /v1/me/desktop-config 拉下来的,策略目录集中定义在 packages/types/src/den/desktop-policies.ts。这些是雇主视角的能力,装之前你该知道自己这台机器上会多出哪一层管控。

许可证是分层的,不能笼统说成 MIT。 仓库根目录 LICENSE 写得明白:/ee 目录下的全部内容按 ee/LICENSE 里定义的许可证走(根 LICENSE 在括号里把它称作 Fair Source License),第三方组件各自沿用原始许可证,只有这两类之外的部分才是 MIT,版权归 Different AI, Inc.。再翻开 ee/LICENSE 本身,抬头写的是 Functional Source License, Version 1.1, MIT Future License,缩写 FSL-1.1-MIT——所以「MIT 开源」这四个字套在整个仓库上是不成立的。而本文讲的那两个工具的核心实现恰恰住在 ee/apps/den-api/src/mcp/ 下——也就是落在 /ee 这一侧。凡是涉及能不能商用、能不能改、能不能内部部署的判断,本文不提供法律意见,一律以许可证原文为准。

最后一条:README 里那句自我定位是项目自己的说法。 仓库 README 把 OpenWork 定位成面向 macOS、Windows 和 Linux 的、某两款商业产品的开源替代品。那是它的定位表述,不是本文的评价,也不构成对能力对等的背书。这个仓库现在有 3490 个受版本控制的文件,apps/ 下 4 个、packages/ 下 12 个、ee/apps/ 下 10 个、ee/packages/ 下 3 个,apps/server/src/ 光顶层就有 138 个 .ts——体量是真的,能不能替代你现有的工作流是另一回事,得你自己跑。

六、上手与避坑清单

别用桌面端的思路去配外部客户端。 文档里明确说,用桌面应用时根本没有 MCP 端点要配,登录后应用会自己注入 Connect 并按当前组织限定权限。会踩是因为有人在桌面端看到某个内部代理地址就往外部客户端里粘——cloud-mcp.mdx 专门警告过那是第一方流程用的同源桌面代理,不要粘进外部客户端。避法:外部客户端只用那个公开的 /mcp/agent 端点。

先确认你的客户端在支持状态表里是什么状态。 会踩是因为把「有配置指南」当成「已验证可用」。仓库把这两件事分得很开,setup only 的备注里写的是原生验证尚未完成。避法:接入前先翻 cloud-mcp.mdx 那张表,心里对可能遇到的 OAuth 毛刺有预期。

换组织别指望在应用里点一下就生效。 会踩是因为令牌里钉死了组织,你在 OpenWork 里切了组织,外部客户端还连着旧的,然后你会看到一堆莫名其妙的权限不足,或者干脆查不到本该存在的资源。避法:按客户端各自的登出再登录流程重认证,登录时在浏览器里选对组织。

搜不到不等于没有。 会踩是因为打分只认词元前缀与完全匹配,你用一个业务黑话去搜,命中率很低。避法:照着连接说明的建议换 2 到 4 组关键词,必要时用 type 把范围收窄到 mcpskills 再搜,别在一次失败后就下结论。

看到 needs_admin_setup 或 needs_signin,那是给人的动作,不是给模型重试的信号。 会踩是因为模型习惯性地换个参数再试一次,结果绕了半天还是过不去。agent.ts 的连接说明专门立了规矩:把这类状态原样转达为下一步的人工动作,别自己发明 OAuth 客户端或凭据的配置步骤。避法:在你自己的系统提示里也重申一遍,让模型把这类结果当作停机信号交回给人,做法可参考 什么时候该把事交回给人

报障时留好那两个 ID。 会踩是因为出了问题只截一张图,服务端根本查不到对应的调用。文档给的做法是:把响应的 X-Request-Id 头,以及 JSON 体里 MCP 的 referenceId 或 OAuth 的 reference_id 一起附上。避法:把这两个字段的记录写进你的日志约定里。

命中限流就按响应说的等。 会踩是因为脚本无脑重试把情况搞得更糟。文档给的处理是按响应里的 Retry-After 值等待后再试。具体阈值会调整,以官方最新说明为准。

别在配置对象里塞密钥。 架构文档把这条列成了安全前提:配置对象里不应该有任何密钥,连接机制才是唯一的凭据存放处。会踩是因为发布技能时图省事把 token 写进正文,而技能内容是要在组织里被搜到、被执行、被当作指令读的。

收个尾

如果你只想判断这个东西值不值得接,按这几条自查一遍:你的客户端在支持状态表里是哪一档;你要用的能力落在 type 的哪一路来源;那类能力在架构文档的类别表里是可执行还是只返回指令性内容;你愿意把哪些第三方授权集中托管出去;以及你要碰的代码落在 /ee 里还是外面。

接着往下读的话,顺序建议是:ee/apps/den-api/src/mcp/README.md 看清楚哪些操作被有意挡掉,ee/apps/den-api/src/mcp/search.ts 看清楚搜索结果为什么长成那样,ee/apps/den-api/src/mcp/agent.ts 看清楚模型到底收到了哪些行为约束,最后 docs/marketplace-capabilities-architecture.md 看清楚各类能力在执行时会返回什么状态。这四份文件读完,你对这条通道能干什么、不能干什么,就有了自己的判断,而不是照抄别人的结论。

本篇属于一个把开源AI 工作流桌面应用 OpenWork逐层拆开讲的系列,整体地图见 OpenWork 是什么:把技能与 MCP 打包成能力的开源桌面应用;沿着这条线往下,还可以看 OpenWork 开源桌面应用上手:三条安装路线与第一次配置要交出什么OpenWork 开源桌面应用实操:把一次聊天变成可发布复用的技能

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。