开源终端 Agent opencode 接 MCP:两类接法与认证避坑

2026-08-04

本文基于 opencode 仓库 commit 7fe9938(2026-08-04)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/anomalyco/opencode 最新代码与文档为准。

**在 opencode 里接 MCP,本地式和远程式的差别不在配置多写几行,而在于两条完全不同的失败路径:本地式的问题几乎全部发生在进程启动那一刻,远程式的问题几乎全部发生在认证握手那一刻。**分不清这一点,你会拿着排查远程的思路去查一个根本没起来的子进程,或者反过来。

opencode 是一个跑在终端里的开源编码 Agent(MIT 许可证,仓库 https://github.com/anomalyco/opencode )。它的 MCP 接入逻辑集中在 packages/opencode/src/mcp/ 这个目录下的六个文件里,加上配置 schema 和会话侧的工具注册,一共不到十处代码。把这十处读一遍,你对着任何一份配置都能预判它会怎么跑。

站内已经有几篇相邻的文章,分工是这样的:MCP 协议本身是什么讲协议层的概念模型,MCP 工具数量怎么控讲工具膨胀之后的取舍原则,MCP server 开发入门讲你自己写一个 server 该怎么起步;这一篇只讲一件事——opencode 这个具体的客户端是怎么把 MCP server 接进来的,代码在哪、行为是什么、哪里会咬人。


一、先认清这十个零件各自管什么

接 MCP 出问题时最费时间的不是修,是找。下面这张表是我按实际读过的路径列的,出问题时按现象直接跳到对应文件。

组成部分它负责什么仓库位置你什么时候会碰到它
配置 schema定义 local / remote 两种条目允许出现哪些键packages/core/src/v1/config/mcp.ts配置里写了个键但不生效,怀疑拼错
连接与状态机建传输、连不上时判定成哪种状态、进程收尾packages/opencode/src/mcp/index.tsserver 显示 failed / needs_auth 时
工具清单与改写拉 tools 列表、分页、把工具名改成合法形式packages/opencode/src/mcp/catalog.ts工具名和 server 文档里对不上时
凭据存储OAuth 令牌、客户端信息、code verifier 的读写packages/opencode/src/mcp/auth.ts想知道令牌存哪、怎么清掉
OAuth 客户端实现动态注册、拿令牌、存令牌、生成 statepackages/opencode/src/mcp/oauth-provider.ts授权服务器嫌客户端元数据不对时
授权回调服务本机起一个 HTTP 服务接授权码packages/opencode/src/mcp/oauth-callback.ts浏览器跳回来一片空白或报 404 时
会话侧工具注册把 MCP 工具变成模型能调的工具、执行前问权限packages/opencode/src/session/tools.ts想知道调用前会不会拦一下
系统提示注入把 server 自带的说明塞进系统提示packages/opencode/src/session/system.ts上下文莫名变长时
命令行入口list / auth / logout / debug 等子命令packages/opencode/src/cli/cmd/mcp.ts手动触发认证、查状态时
官方文档配置项表格与几个现成示例packages/web/src/content/docs/mcp-servers.mdx想确认某个键是必填还是可选

配置入口只有一个:配置文件里的 mcp 对象,每个 server 起一个唯一名字。名字不是装饰,后面工具名、权限规则、命令行参数全靠它。


二、本地进程式:它其实就是替你 spawn 了一个子进程

本地式的配置形状,文档里给的示例是这样的:

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "my-local-mcp-server": {
      "type": "local",
      "command": ["npx", "-y", "my-mcp-command"],
      "enabled": true,
      "environment": {
        "MY_ENV_VAR": "my_env_var_value",
      },
    },
  },
}

schema 里 local 这一支,除了标记类型的 type,允许的键一共五个:command(必填,字符串数组)、cwdenvironmentenabledtimeout。两种条目在 schema 里是按 type 分支的联合类型,所以远程式的键写进本地式条目里不会生效——查”我明明写了却没用上”的时候,先回到这个文件确认这一支到底认哪些键。

代码这一侧做的事非常直白。connectLocalcommand 数组的第一项当命令、其余当参数,工作目录取 cwd(相对路径按工作区目录解析,没写就用工作区目录本身),然后建一个标准输入输出传输去连。有两个细节值得你记住。

第一个是环境变量的合并顺序。传给子进程的环境是当前进程的全部环境变量,再叠上你在 environment 里写的那些。意思是:你 shell 里所有的密钥、令牌、内网地址,都会原样进入这个 MCP server 进程。这不是 opencode 独有的做法,但它是你引入一个陌生 MCP server 时真实存在的暴露面。一个第三方 server 拿到你的环境变量之后做什么,opencode 管不着。

第二个是进程收尾。这段收尾挂在 MCP 服务实例的作用域上,实例关闭时才跑一次,不是每结束一个会话就跑一次。清理逻辑里,对每个用标准输入输出传输的客户端,先取它的进程号,再递归地把后代进程找出来逐个发终止信号,最后关闭客户端。找后代用的是 pgrep -P——而这段逻辑在 Windows 平台上直接返回空列表。所以在 Windows 上,如果你的 MCP server 是一个包装脚本(比如 npx 拉起真正的实现),最外层进程会被关掉,里层那个不一定。装完一批 MCP 之后发现机器上挂着一堆孤儿进程,多半是这个原因。


三、远程式与认证:401 之后发生的六件事

远程式的配置键是 url,可选 headersoauthenabledtimeout。最简单的用法是自带 API key 走请求头,同时把 OAuth 关掉:

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "my-api-key-server": {
      "type": "remote",
      "url": "https://mcp.example.com/mcp",
      "oauth": false,
      "headers": {
        "Authorization": "Bearer {env:MY_API_KEY}"
      }
    }
  }
}

连接过程分两跳:先试可流式的 HTTP 传输,失败了再试 SSE 传输。注意这个降级不是无条件的——如果第一跳失败的原因被判定成认证问题,循环会直接中断,不再试第二种。这个设计是对的(认证不过换个传输也不会过),但会造成一个观感:明明配置里两种传输都支持,日志里却只看到一次尝试。

OAuth 这条链路是 opencode 帮你做得比较多的地方。按代码里的流程,401 之后依次发生:

  1. 抛出的错误被识别为未授权,或者错误信息里带 OAuth 字样;
  2. 如果错误信息里出现了注册相关或客户端标识相关的字眼,状态被判成需要客户端注册,并弹一条提示,让你在配置里补 clientId
  3. 否则状态判成需要认证,把这个传输实例挂进待办表,弹一条提示告诉你去跑对应的认证命令;
  4. 你触发认证后,本机会起一个回调 HTTP 服务,默认监听在 127.0.0.1 的 19876 端口、路径 /mcp/oauth/callback,这两项可以用 callbackPortredirectUri 改;
  5. 生成一个随机的 state 存起来,打开浏览器让你授权,等回调;回调等待有五分钟的超时;
  6. 回调带回授权码,比对 state,一致才继续换令牌,不一致直接报可能存在跨站请求伪造并清掉状态。

state 的校验做了两层:回调服务这一侧,请求里没有 state 参数直接返回 400,state 在待办表里找不到也返回 400;上层拿到授权码之后再和自己存的那个比一次。这类流程里 state 校验被省掉是常见的偷懒,这里没省。

动态客户端注册那一支也在。如果你没提供 clientId,客户端会尝试按 RFC 7591 去注册,注册时提交的元数据里客户端名称写的是 OpenCode、客户端地址指向项目官网,授权类型申请的是授权码加刷新令牌,响应类型是 code;令牌端点的认证方式取决于你有没有配 clientSecret——配了就用表单提交密钥的方式,没配就声明成公开客户端。有些企业侧的授权服务器不接受公开客户端,这时你就必须走预注册那条路,把 clientIdclientSecret 写进配置。

令牌落在哪:一个名为 mcp-auth.json 的文件,位于 opencode 的数据目录下(文档里给的路径是 ~/.local/share/opencode/mcp-auth.json),写入时文件权限被显式设成 0o600。存储结构里除了访问令牌、刷新令牌、过期时间、scope,还额外记了一个 serverUrl。这个字段是有用的:读凭据的方法会先比对当前配置里的 URL 和当初存令牌时的 URL,对不上就当作没有凭据。也就是说,你把某个 server 的 url 改了之后,旧令牌不会被误发到新地址去。

还有一个容易忽略的设计:认证进行中用的是一个”挂起态”的凭据提供者,它把令牌和动态注册拿到的客户端信息先扣在内存里,只有整条流程走通之后才一次性提交落盘。半路失败不会在磁盘上留下一份残缺的令牌,这一点在反复调试认证时很省事。

但有一点要说清楚,别把”挂起”理解成整条链路都不落盘:PKCE 用的 code verifier 和防跨站的 state 是照常写进凭据文件的,前者要等认证走完才被清掉,后者在比对完之后清。也就是说,一次失败的认证会在那份文件里留下这条 server 的记录,只是里面没有令牌。真要从一个干净状态重来,别只是重跑一次认证命令,先用 logout 子命令把这条记录整个删掉,再从头走一遍。


四、工具到达模型之前,被加工了三次

这一节决定了你在提示里该怎么称呼一个工具,也解释了大部分”我明明装了但模型不会用”的疑惑。

第一次加工是改名。 工具的最终名字由 server 名和工具原名拼成,中间一个下划线,两段各自都会先做一次清洗:非字母、数字、下划线、连字符的字符全部替换成下划线。所以你在 server 文档里看到的工具名,未必是 opencode 里那个名字。文档里也点明了这个前缀规则:想一次性关掉某个 server 的全部工具,用 "服务器名_*": false 这种通配写法。

顺带一个真实的坑:清洗只保留字母、数字、下划线和连字符,其余字符统统变成下划线。也就是说 a.ba ba@b 清洗完都是 a_b,跟本来就叫 a_b 的撞在一起;下划线和连字符本身在白名单里,不会被换。两个 server 或两个工具的名字只差一个标点时,先想到这一层。

第二次加工是列表拉取本身。 拉工具清单走的是分页循环,最多一千页,如果服务端返回了重复的游标会直接报错中断——防的是服务端分页实现有 bug 时把客户端拖死。还有一处容错:某些 server 的输出 schema 里带无法解析的引用,标准解析会失败,这时会用一个把输出 schema 剔掉的宽松 schema 重新请求一遍。这意味着 server 写得不太规范时 opencode 也能把工具列出来,但输出 schema 就不带了。

第三次加工是执行包装。 每个 MCP 工具被包成一个动态工具,调用时带上超时、允许在收到进度通知时重置超时;为了让 SDK 真的下发进度令牌,代码里挂了一个空的进度回调(SDK 只在有这个钩子时才发进度令牌,注释里写明了这个理由)。返回结果如果被标记为错误,会把所有文本内容拼起来抛异常;如果内容为空但有结构化内容,则把结构化内容序列化成一段文本补上——不然模型会收到一个空结果,然后开始瞎猜。

除了工具,server 自带的说明也会被用上:连上之后取出 server 的 instructions,在系统提示里以一个专门的区块注入。这里有个按权限过滤的细节——如果某个 server 的工具全部被权限规则禁用,它的说明就不注入了。省下来的是上下文。

工具执行前会走一次权限询问,权限标识就是那个拼出来的工具名。你可以按名字配规则,也可以配成一律放行。放行之前想清楚:这等于让模型无需确认就能调那个外部服务,写操作也包括在内。这块的取舍可以参考最小权限怎么设计


五、接多了之后,最先出问题的是这三样

官方文档在最前面就放了一段告诫:MCP server 会往上下文里加东西,工具一多很快就堆起来,所以要谨慎挑选启用哪些;并点名说某些 server(文档举的例子是 GitHub 那个)token 消耗大,容易把上下文顶爆。这是排在第一位的问题,而且是最没有技巧可言的一个——工具描述必须进上下文,进了就要占。

第二样是启动时间。所有配置里的 server 是并发去连的,并发度不设上限。并发本身是好事,但每个 server 各自的超时是独立计的,任意一个卡住都会让你在启动阶段干等。这里有个值得核对的地方:文档的配置表格里写 timeout 默认 5000 毫秒,而连接与工具拉取代码里的默认超时常量写的是 30000。两处对不上,实际以你读到的代码为准;真在意的话就显式写 timeout,别依赖默认值。

第三样是名字与状态的管理成本。opencode 给 server 定义了五种状态:已连接、已禁用、失败、需要认证、需要客户端注册。前三种好理解,后两种是 OAuth 特有的。连接断开时会把状态改成失败并附上原因,同时把该 server 的工具从缓存里清掉、广播一次工具变更事件;server 主动通知工具列表变化时也会重新拉一次。状态是活的,不是启动时定死的——所以你看到的状态和五分钟前不一样,属于正常。

关掉工具的手段有两层:全局用 tools 配置按名字或通配关掉,再在某个 agent 的配置里单独打开。文档里推荐的做法就是全局关、按 agent 开。server 多了之后这几乎是唯一可行的组织方式,否则每个会话都在为用不上的工具付上下文。这个取舍在MCP 工具数量怎么控里有更一般化的讨论。

还有一个实验性开关值得知道:环境变量 OPENCODE_EXPERIMENTAL_CODE_MODE 打开后,会话侧不再把 MCP 工具一个个注册成模型工具,而是按 server 名组织成命名空间,走另一条调用路径。实验性质的东西不适合直接上生产,但它说明了一个方向——工具数量的压力,最终要靠改变暴露方式来解,而不是靠克制。


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

它不替你审 MCP server。 本地式意味着 opencode 会用你的身份、你的环境变量、你的工作目录启动一个你指定的进程;远程式意味着你的请求头和令牌会发到你写的那个地址。装一个 MCP server 的信任级别,等同于装一个能读你环境变量的 CLI 工具。仓库里没有、也不承诺有对 server 行为的沙箱或审计。

它不管 server 的质量。 工具描述写得含糊、参数 schema 不规范、返回一大坨 JSON,这些都会直接变成模型的困惑和你的 token 账单。前面提到的宽松解析和结构化内容兜底,只是让不规范的 server 别把流程整崩,不是把它变好。

它不解决”这个工具该不该被调”的判断。 权限询问给的是拦一下的能力,不是判断力。真要控住,还是得靠权限规则和 agent 级别的工具开关。

Windows 上的进程清理是残缺的。 前面说过,找后代进程那段逻辑在 Windows 平台直接返回空。这是明写在代码里的平台分支,不是 bug 报告里的传闻。

认证只覆盖远程式。 本地式没有 OAuth 这条链路,凭据全靠 environment 传——也就是明文写在配置文件里,或者引用环境变量。配置文件如果进了版本库,那就是一次泄漏。

再说一句更大的边界:opencode 这类工具本身就会在你的机器上执行命令、直接改你仓库里的文件、把代码内容发给模型服务商。接 MCP 只是在这个已有的暴露面上又开了几个口子。误删误改、私有代码外泄、密钥被带进不该去的进程,这些风险在接第一个 MCP server 之前就已经存在了,接完之后只是变大。各家模型服务商对数据的处理规则不同且会调整,以官方最新说明为准。


七、上手与避坑清单

先用一个玩具 server 打通链路,再接真的。 会踩是因为你同时在验证三件事:配置写对没、server 能不能起、工具能不能被调。三件事一起验,失败了不知道该改哪。文档里给了一个专门用来测试的 server 示例(@modelcontextprotocol/server-everything),先用它把链路跑通,之后再换成你真正要接的。

本地式先在终端里手动跑一遍那条命令。 会踩是因为 command 数组里第一项是可执行文件、其余是参数,写成一整个字符串或者路径里有空格,表现出来就是一个笼统的连接失败。避法:把 command 数组拼成命令行,在同一个工作目录下亲手执行一次,能起来再往配置里放。

远程式先分清是没连上还是没认证。 会踩是因为两种失败在感知上都是”用不了”。避法:用命令行的 list 子命令看状态——落在需要认证还是需要客户端注册,是完全不同的两条修法;落在失败,那就是网络或地址的问题。项目还提供了一个 debug 子命令用来诊断连接和授权流程。

授权后浏览器跳回来是 404,先查回调地址。 会踩是因为回调服务只认它当前配置的那个路径,路径不对直接 404;而 callbackPort 只是改端口的简写,一旦你写了完整的 redirectUri,端口简写就被忽略。避法:要么两个都不写用默认,要么只写 redirectUri 一个,别混着写。

换了 server 的 URL 之后先清凭据。 会踩是因为凭据是按 server 名字存的,但读取时会比对当初记下的 URL。URL 一改,旧令牌被判定为不可用,表现是明明之前认证过却又要求认证。避法:改 URL 时顺手跑一次 logout 子命令把旧凭据删掉,别对着”为什么又要登录”发呆。授权凭据过期的一般性处理见授权过期怎么办

别依赖 timeout 的默认值。 会踩是因为文档表格和代码常量对不上,你按文档理解的行为可能和实际差好几倍。避法:对慢 server 显式写一个你能接受的值,写在配置里,别让它成为一个需要读源码才知道的隐含前提。

工具名以 opencode 里的为准,不以 server 文档为准。 会踩是因为名字被拼过前缀、清洗过特殊字符。要注意 list 子命令列的是 server 和它们的状态,不是工具名。避法:接完之后让模型把可用工具列一遍,或者对着 server 文档里的原始工具名按”server 名 + 下划线 + 工具名、两段都清洗过”的规则自己推一遍,把真实名字抄下来,再去写权限规则和通配符。规则写错了不报错,只是不生效,这种沉默失败最耗人。

新接一个 server,先全局关掉再按 agent 开。 会踩是因为默认接进来就是全会话可见,上下文的账要等你发现响应变慢、成本变高时才结。避法:接进来第一件事是在 tools 里用通配把它关掉,只在真正需要它的那个 agent 的配置里打开。


接下来该读哪个文件,取决于你卡在哪一环:卡在配置不生效,读 packages/core/src/v1/config/mcp.ts,那里是允许键的唯一真相;卡在连不上或者状态莫名其妙,读 packages/opencode/src/mcp/index.ts 里的两个连接函数和状态定义;卡在认证,oauth-provider.tsoauth-callback.ts 两个文件加起来不到五百行,一口气读完比试错快;卡在工具名和调用结果,读 catalog.ts

一份配置上线前,对着这四个问题过一遍就够了:这个 server 拿到我的全部环境变量,我接受吗;它的工具在最坏情况下能做什么,权限规则拦不拦得住;它的工具描述加起来占多少上下文,值不值;它挂掉的时候,我从哪看出来是它挂了。四个都答得上来,再接下一个。

本篇属于一个把开源AI 编程 Agent 项目 opencode逐层拆开讲的系列,整体地图见 opencode 是什么:终端里的开源 AI 编程 Agent 全景地图;沿着这条线往下,还可以看 给 opencode 写自定义命令:把重复指令固化成一条斜杠命令终端编码 Agent opencode 的 skills 用法与分工

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