真实 Bug 排查全过程:事件流未关闭 → 超时冲突 → 端口消失 → 客户端识别失败 → SSE 假警报。揭示分布式系统中日志盲点和调试方法论。

这些原因按我们实际发现的顺序列出,包括其间的虚假胜利。
用户点击授权。邮件代码送达,他们输入代码,浏览器重定向。Codex 说:"verification failed"。
让这个问题变得特别糟糕的是:我们整个流程的服务器日志中只有 200、201 和 302。没有任何一个请求出现错误。失败的每一跳都对服务器端隐形。
直到将访问日志时间戳、屏幕录像和用户代理遥测数据排在一个时间线上,这个链条才有了意义。
在验证期间,Codex 对 MCP 端点发起了一个裸 GET 请求——并等待响应完成。
我们的端点用一个永不关闭的 keepalive 事件流来回答那个 GET。我们故意这样做的:mcp-remote 的探测 GET 在报头上与遗留 SSE 客户端相同,所以你无法区分它们,关闭流会破坏那些用户。
所以 Codex 的验证器就那样坐着,直到它自己的多分钟超时期限过期。
坚持下来的修复是基于 Accept 语义进行分裂,而不是试图识别客户端:
if "text/event-stream" not in accept:
# 不要求流——回答并关闭 (11ms in prod)
else:
# 保持流打开,如 mcp-remote 所期望
修复它。重新尝试。仍然失败。
那个长达数分钟的停滞不仅仅是慢——它比 Codex 在 127.0.0.1:. 上打开的一次性 OAuth 回调监听器的生命周期还要长。
到用户完成输入邮件代码的时候,服务器完美无缺的 302 重定向到了一个没有人在监听的端口。
这就是虚假"verification failed"的全部解剖:每一个破裂的跳转都是客户端超时,服务器从未看到任何问题。
修复第 1 层后,监听器窗口应该会存活。现在有些重新测试通过了。有些没有。这就引出了我最不引以为豪的一层。
在 macOS 上,退出桌面应用只会杀死 UI。一个应用服务器守护进程继续在下面运行,与来自之前尝试的残留浏览器标签页一起,它乐意将死亡的身份验证会话复活到我的"干净"重新测试中。
我在真正是之前运行结果的人工结果上浪费了尴尬的大量时间。
无聊的修复:在信任任何重新测试之前,pkill 整个进程族。如果你的 OAuth 调试感觉不确定,首先检查谁还活着。
pkill -9 <process-family>
在盯着监听器窗口时,我们不得不承认我们的授权页面是问题的一部分——三个入口路径,包括一个 API 密钥粘贴框,而且速度足以在超时上翻滚。
所以我们调查了八个运行远程 MCP 连接器的供应商如何做(Notion、Linear、Sentry、Atlassian、GitHub、Asana、Stripe,加上 Cloudflare 的 oauth-provider 默认值):
我们重建了我们的以匹配:一键,大约两秒。更快、更清晰、在目录审查中更好。
而且 codex mcp login 仍然失败。
每个环境都被排除了,所以剩下的必须在协议交换本身中。
我们的授权服务器元数据声明:
"authorization_response_iss_parameter_supported": true
RFC 9207。以及准确的声明——我们确实在每个重定向上发送 iss,包括错误分支。
相关的 Codex 构建读取该声明并强制执行它:回调必须携带 iss。它的 loopback 监听器虽然是较旧的代码,根本不解析 iss。
声明该功能,被其执行。
证明这需要排除所有其他内容,因为浏览器自动打开意味着你永远不知道什么实际上首先命中监听器——而一次性回调通道由其第一个请求决定。
两个技巧使其工作:
codex mcp login 无头编写整个流程,所以官方 CLI 成为了测试工具。BROWSER 环境变量对此代码路径没有任何作用(webbrowser::open 忽略它——问我怎么知道的),所以 PATH shim 把 open 变成了无操作。然后我手工构建了回调并从服务器端将其传递给 loopback 端口。
监听器在其生命周期中恰好接收一个请求:code、iss、state,全部存在,全部正确。
响应:缺少 issuer。
此时没有什么可怪的了,除了解析器。对照组:Claude Desktop 没有这样的强制——同一个服务器,第一次尝试通过。
一行:停止声明该功能。
"authorization_response_iss_parameter_supported": false
我们仍然在每个重定向上发送 iss。我们只是不再宣布它——而没有宣布的就不会被强制。
codex mcp login: Successfully logged in.
当一个构建发货包含解析 iss 的监听器时,我们将重新声明——在用 CLI 测试后,而不是在读了更改日志后。
你在元数据中声明的每项功能都是某个客户端会对你强制执行的合同。声明表面不是一个完整性竞争。准确的声明加上有 bug 的客户端等于你的中断。
而当一个流程以无可挑剔的服务器日志失败时,停止读服务器日志。从客户端构建时间线——服务器永远不会坦白。
我构建了 Humaux Memory,一个远程 MCP 服务器,为 AI agent 提供共享的长期记忆。这些笔记存在其中,这就是 dogfooding 循环。很高兴在评论中回答问题——特别是如果你现在正在用干净的日志盯着"verification failed"的话。
若要进一步行动,你可考虑屏蔽此人和/或报告不当行为