MCP 工具为什么老让你重新登录:刷新令牌的生命周期与四处修复


上个月你把 Linear、Figma 和 Notion 都接进了 Hermes,hermes mcp list 里三个服务器一切正常,工具调用也很顺手。今天早上打开电脑,每个 MCP 工具都开始报授权错误;Desktop 昨晚弹过一次提示,你当时正在开会,顺手关掉了。你把 Linear 重新登了一遍,下午 Figma 又掉了——而你在笔记本和办公室那台机器上各开着 Hermes 的时候,这事几乎每周都会来一次。你开始怀疑是不是自己配置写错了,其实不是:问题出在这类服务器背后那条长长的令牌生命周期上,而 Hermes 最近连做了四处修复,正是冲着它去的。

先搞清这条链路:访问令牌、刷新令牌和“一次一用”

MCP(Model Context Protocol)是一套让 Hermes 调用外部服务所提供工具的协议;Linear、Figma、Notion 这类服务器不认静态密钥,而是走 OAuth 授权。授权完成后你拿到两样东西:

  • 访问令牌(access token):寿命很短,通常几十分钟到一小时,每次调用工具时都要带上它。
  • 刷新令牌(refresh token):寿命很长,唯一作用就是去换一枚新的访问令牌。

为了让被偷走的刷新令牌尽快失效,现代授权服务器普遍启用轮换(rotation):你每用刷新令牌换一次访问令牌,旧的那枚立刻作废,同时下发一枚新的。这个设计本身很合理,但它有个配套的保险机制——重放检测(reuse detection):如果同一枚刷新令牌第二次被送上门,服务器无法分辨这是网络重试还是凭证已经泄露,于是采取最安全的处理,直接吊销整组授权(这台客户端配这个用户的所有令牌一起失效),然后要求你重新登录。

问题就出在这里。同一台机器上跑着 Desktop、dashboard 和后台网关,或者你开了两个 profile,它们共用同一个令牌文件 ~/.hermes/mcp-tokens/linear.json。当访问令牌过期,两个进程几乎同时看到它过期,各自拿着同一枚刷新令牌去换:

  1. 进程 A 先到,换到新令牌,旧的刷新令牌当场作废;
  2. 进程 B 慢了几百毫秒,提交的还是那枚已经作废的刷新令牌;
  3. 服务器判定重放,吊销整组授权——A 手上那枚刚拿到的、完全健康的新令牌也一起报废。

两边一起掉线,而且看起来毫无道理:明明刚才还能用。这就是 PR #111186 要解决的问题。

修复一:跨进程的刷新围栏,让一枚刷新令牌只出示一次

Hermes 在令牌文件旁边加了一把跨进程锁,业内通常叫围栏(fence):文件锁由内核提供(Linux/macOS 上是 flock,Windows 上是 msvcrt.locking),锁的是操作系统层面的文件句柄,不靠“文件是否存在”来判断有没有别人在跑,这样即使进程崩溃留下残留文件也不会误判。锁文件是令牌文件的附属文件,名字形如 linear.json.refresh.lock。

整个“读刷新令牌 → 向授权服务器发请求 → 把结果写回磁盘”的序列都被圈在这把锁里面,于是行为变成了:

  • 拿到锁之后先重新读一次磁盘。 如果同伴进程刚刚完成轮换,并且它写回的那对令牌还有效,那么当前进程直接采用同伴的新令牌并跳过请求——不重复出示那枚已经用掉的旧令牌。
  • 如果同伴那对令牌已经过期,就用同伴留下的刷新令牌去换(它才是当前唯一有效的那一枚),而不是用自己内存里那份陈旧副本。
  • 续期时间缺失按“还活着”处理:服务器没有明确给出有效期时,不擅自判定它已失效。
  • 拿不到锁就失败关闭:等待超时后这次刷新会明确报错,而不是赌一把去重放令牌。被占用之外的错误码一律立即失败,不会被误当成“有同伴在跑”而无限等待。

这对你意味着什么: 你可以同时开着桌面端、dashboard 和一个后台网关,也可以在两个 profile 里各连一个 Linear,不必再靠“只开一个 Hermes”来躲避掉线。掉线从“随机发生、原因不明”变成“要么正常工作,要么明确报一次刷新失败”。

修复二:刷新令牌被钉在签发它的授权服务器上

下一个隐患更安静。OAuth 授权服务器会在自己的元数据(metadata)里声明自己的身份地址,这个值叫 issuer。Hermes 通过 MCP 服务器公开的地址去发现元数据——如果这个地址背后换了角色(服务迁移、运维改了元数据、更糟的情况是域名被劫持),Hermes 就会把一枚长期有效的刷新令牌交给一台当初根本没签发过它的服务器。刷新令牌的价值远高于访问令牌,这种移交等于把长期凭证送给陌生人。

现在的做法是:Hermes 在缓存令牌旁边多记一个 hermes_issuer 字段,写下这枚刷新令牌当初是谁给的。这个字段纯属 Hermes 自己的记账信息,送给 SDK 做令牌校验之前会先摘掉,也永远不会出现在任何网络请求里。每次连接 MCP 服务器初始化时,Hermes 拿存储的 issuer 和这次发现的 issuer 比对:

  • 不一致 → 丢弃刷新令牌(内存和磁盘上都清掉),但保留仍然有效的访问令牌,因为它对原来的签发者依然有效,可以继续用到过期为止;过期之后走一次正常的重新授权,这次授权指向新的 issuer。
  • 一致 → 什么也不做,继续用。
  • 老令牌文件没有这个字段(升级前就存在的安装)→ 第一次采用当前发现的 issuer 记录下来,而不是一脚把你踹去重新登录。这是有意为之的选择:Hermes 已有大量历史安装,令牌文件早于这个字段诞生,强制重登会造成毫无必要的打扰。

顺手记一笔:这里和上游 codex 的实现(openai/codex 的同类改动)故意不同,codex 因为该字段随存储一起发布,遇到缺失就直接硬失败。

修复三:RFC 9207 与 Figma 那一处例外

RFC 9207 是一条 OAuth 扩展标准:授权服务器在把浏览器重定向回客户端时,附带一个 iss 参数声明“我是谁”。客户端凭它可以确认这次回调确实来自自己信任的那台授权服务器,而不是被别人塞进来的另一家的授权码——这防的是授权服务器混淆类攻击。

这里有两个方向相反的坑,Hermes 各修了一次:

坑一:中继丢了 iss。 当授权服务器在元数据里声明支持这个参数,而回调里没有带上它时,底层 SDK 会拒绝并报出 OAuthFlowError: Authorization response missing iss parameter。CLI 路径一直是对的,但桌面端、dashboard 和 TUI 的回调中继没有把 iss 一路带到 MCP 客户端,于是在 Cloudflare、Resend 这类规范实现 RFC 9207 的服务器上,从图形界面登录就是死路一条。现在每条中继都完整传递这个参数。

坑二:Figma 说话不算话。 Figma 的授权服务器(https://api.figma.com)声明自己支持 iss,实际却从不发送它——结果是每一枚合法的授权码都被 SDK 丢掉,hermes mcp login figma 永远走不完。Hermes 对这一个 issuer 开了口子:回调里带了授权码但缺 iss 时,用发现到的元数据 issuer 补上,同时记一条警告日志;如果 iss 存在但与预期不符,仍然照常拒绝;其他所有服务器继续执行严格规则。没有开关,也没有新增环境变量。

修复四:桌面端每天提醒一次该重新登录的服务器

令牌死掉之后,被“停靠(parked)”的服务器不再尝试打开浏览器——因为后台没人能点同意。这本来是好事,但副作用是它变得完全隐形:健康检查在状态转坏的那一刻提示一次,随后就在本次应用会话里彻底闭嘴。你周一关掉那条提示,服务器就一直躺着,直到你偶然打开 Capabilities 页面才发现。

现在桌面端改成每天提醒一次:进入异常状态时提醒,之后只要 24 小时的静默期过去、服务器仍然不可用,就再提醒一次;令牌健康时永远不提醒。提示里有两个按钮:

  • Sign in —— 跳到 MCP 页面已有的授权按钮,不会在后台自动拉起浏览器标签;
  • Disable —— 调用已有的接口给这个服务器写上 enabled: false。网关的配置协调任务大约 60 秒内把它拆掉,hermes serve 后端则在下次 /reload-mcp 时丢弃它。

静默期按 profile 和服务器分别记录,所以你不会被同一个服务器一天骚扰十几次,也不会因为它曾经提醒过就永远错过。

每个改动分别落在哪个 PR

PR 标签 行为变化
#95246 type/security(2026-09-14) 存储的刷新令牌不再能被发给别的 issuer:不匹配时丢弃刷新令牌、保留未过期的访问令牌;老令牌文件首次采用当前发现的 issuer
#111186 type/bug,P1(2026-09-15) 刷新跨进程加围栏:轮换后的刷新令牌只出示一次,采用同伴的新令牌并跳过请求,锁超时失败关闭
#111628 type/bug,P2(2026-09-15) RFC 9207 服务器上的 OAuth 登录在 dashboard、桌面端和 TUI 都能完成(Cloudflare、Resend)
#112059 type/bug,P2(2026-09-15) Figma 省略 iss 参数时登录仍能完成;严格规则只对这一个 issuer 放宽
#110078 type/feature(2026-09-13) 桌面端对需要重新授权的 MCP 服务器每天提醒一次,带 Sign in / Disable
#111481 修复(2026-09-15) 由 dashboard 提供的 profile 保有自己的 MCP 连接与凭据,不再串用别的 profile 的登录态

遇到 MCP 工具突然要你重新登录时怎么办

按顺序来,通常一两步就能恢复:

  1. 先看提示里的原话。 “刷新失败”和“issuer 变了”是两类问题,前者多半是令牌过期或授权被吊销,后者说明服务器那一侧的授权地址换了。
  2. 确认是哪个服务器。 hermes mcp list 会显示被停靠的服务器,hermes mcp test linear 可以单独探一次连接。
  3. 重新授权一次。 hermes mcp login linear 重登单个服务器,hermes mcp reauth --all 把所有 OAuth 服务器一起重登。桌面端直接点提示里的 Sign in 即可。
  4. 让当前会话看见新令牌。 会话的工具集在开始时就被冻结,重登之后用 /reload-mcp,或者在桌面端开一个新会话。
  5. 两台机器都升级。 围栏是进程间的协作机制,只升级其中一台,另一台仍会照着老方式重放令牌。确保两边都包含 PR #111186 的版本。
  6. 后台网关永远不会打开浏览器。 如果你在服务器上跑着网关,掉线只会留下日志警告,需要你在别处完成一次授权,然后在下次探测时被接上。
  7. 实在不行就删掉令牌文件重登。 文件按服务器名存放在 ~/.hermes/mcp-tokens/ 下,删掉对应的那个再跑一次 hermes mcp login 是最干净的复位方式。
  8. 不用了就直接 Disable。 别让一个早就废弃的服务器每天提醒你一次。

如果你是在没有浏览器的远端服务器上做授权,流程和这里不太一样——端口转发、粘贴回调、设备码那三条路我在这篇里写全了:远程服务器上怎么完成 OAuth 登录。命令细节(login / reauth / test / catalog 等子命令的完整参数)见 hermes mcp 命令参考。想把配置写对、少踩坑,远程 MCP 官方目录里那份清单也值得先翻一遍。

小结

这四处修复其实是同一条原则的三张面孔,外加一次人性化补丁:凭证只能交给签发它的那台服务器(issuer 绑定)、轮换后的刷新令牌只能被消费一次(跨进程围栏)、回调身份必须在每条链路上校验到底(RFC 9207,含 Figma 那一处例外),以及需要你介入的事必须被说出来(每日提醒)。

对使用者来说,结论很朴素:MCP 掉线不再是“玄学”,你可以放心地同时开桌面端、dashboard 和网关;真的需要重新登录时,你会在当天看到提示,而不是某天发现工具全都不能用了。升级到带这些修复的版本(hermes update)即可生效,v0.21.2 稳定性补丁之后的内容里已经包含它们。