中文 English

MCP 明明没配错,为什么一启动就挂?一次 Codex MCP 故障的完整拆解

发布时间: 2026-05-23
MCP Codex AI Agent macOS Swift 动态链接 故障排查 开发工具

先说结论

这次故障最容易误判的地方,是表面错误写着 “MCP Client 启动失败”“MCP Server 握手失败”,于是人很自然会去怀疑 MCP 配置、插件开关、网络、Token、JSON 格式、stdio 协议,甚至怀疑 Codex 本身挂了。但真正的根因并不在 MCP 协议层:某个 MCP Server 对应的本地二进制在启动瞬间就被 macOS 动态链接器杀掉了,根本没有机会完成 initialize 握手。Client 看到的只是连接提前关闭,Server 端真正留下的线索是 dyld: Symbol not found,缺的是 Swift Concurrency runtime 里的一个符号。

换句话说:MCP 没有来得及失败,Server 进程先死了。

这篇文章记录一次很典型、也很容易绕远路的 Agent 工具链故障:Codex 里 MCP Client / MCP Server 启动异常。它不是一个“修改配置就好”的问题,而是一个从应用层错误一路追到本地二进制、动态链接器、Swift runtime 兼容性的排障过程。

为了避免泄露任何隐私信息,本文不包含真实内网地址、真实用户名、真实会话 ID、真实本机绝对路径、Token、私有仓库地址或任何业务系统名称。文中路径都使用 ~<USER_HOME><PLUGIN_DIR><PROJECT> 等占位符表示;命令输出也只保留与判断根因有关的公开技术字段。

MCP 故障不一定发生在 MCP 协议层

图 1:看起来是 MCP 握手失败,真正的问题可能发生在更底层。

1. 问题背景:MCP 已经变成 Agent 的“外设总线”

现在很多 AI Agent 工具已经不再是单纯的聊天窗口。它们会读取文件、执行命令、访问浏览器、连接 GitHub、操作日历、调用数据库、控制本地应用。为了让这些能力以统一方式接入,MCP,也就是 Model Context Protocol,逐渐变成一种常见的“外设总线”。

在一个典型的 Codex 或类似 Agent 环境里,MCP 可能承担几类工作:

  1. 通过本地 stdio server 暴露文件、浏览器、电脑控制、图片处理等能力。
  2. 通过 HTTP 或 streamable HTTP server 暴露远端连接器,例如代码托管、文档系统、协作工具。
  3. 把工具描述、参数 schema 和调用结果交给 Agent,让模型能选择并调用这些工具。
  4. 在会话启动、工具刷新、关闭清理时完成初始化、握手、能力枚举和资源释放。

这套结构非常灵活,但也带来一个排障难点:当用户看到“某个 MCP 启动失败”时,失败点未必在 MCP 协议本身。它可能是配置文件格式错,也可能是环境变量没传进去,也可能是 Node、Python、uv、npx、动态库、系统权限、网络代理、证书、登录态、系统版本兼容性的问题。

这次案例就属于最后一种:MCP 层只负责报告“握手失败”,根因在本地二进制启动阶段。

2. 问题表现:Client 说握手失败,Server 只留下一行 dyld

一开始能看到的症状并不复杂。Codex 主程序和 app-server 都在运行,普通对话、终端命令、日志读取也正常。但是某些 MCP 工具无法正常初始化,日志里出现类似下面的警告:

failed to initialize MCP client during shutdown:
MCP startup failed:
handshaking with MCP server failed:
connection closed: initialize response

另一次工具刷新阶段也能看到类似信息:

failed to force-refresh tools for MCP server '<server-name>', using cached/startup tools:
failed to get client:
MCP startup failed:
handshaking with MCP server failed:
Transport channel closed

如果只看这两行,很容易顺着 MCP Client 的方向查:是不是 Client 没连上?是不是 server URL 错了?是不是 JSON-RPC 初始化包格式不对?是不是某个连接器需要重新登录?是不是代理断了?

但真正关键的日志来自 MCP server 的 stderr。某个本地 server 启动时输出了这样的错误:

dyld: Symbol not found: _swift_task_addPriorityEscalationHandler
Referenced from: <PLUGIN_DIR>/SkyComputerUseClient
Expected in: /usr/lib/swift/libswift_Concurrency.dylib

这几行信息非常关键。dyld 是 macOS 的动态链接器。它在进程真正运行用户代码之前负责加载二进制、解析动态库、绑定符号。如果它说 Symbol not found,说明程序还没进入 MCP server 的业务逻辑,甚至还没机会读配置、建立 stdio、响应 initialize。它在操作系统装载阶段就失败了。

所以这个故障从“协议握手失败”变成了“本地可执行文件运行时依赖不满足”。

MCP initialize 之前,Server 进程已经退出

图 2:Client 等的是 initialize response,但 Server 在发出 response 前已经被系统终止。

3. 第一条原则:不要看到 MCP 就只查 MCP 配置

排这种问题时,我最先确认的不是“怎么修”,而是“到底哪一层失败”。因为多组件系统里,报错出现在哪里,不等于根因就在哪里。

这次链路至少有四层:

层级 负责什么 可能的失败
Codex / Agent Client 读取可用工具,创建 MCP client,会话中调工具 配置未加载、工具缓存旧、Client 关闭时竞态
MCP Transport stdio 或 HTTP 通道,发送 initialize 等消息 通道关闭、代理失败、超时、权限问题
MCP Server 进程 真正暴露工具能力的本地或远端服务 启动命令错、依赖缺失、环境变量缺失
操作系统 / Runtime 加载二进制和动态库 架构不匹配、系统版本不兼容、符号缺失

如果第一层说“handshaking failed”,我们只能知道 Client 没收到完整握手结果。至于为什么没收到,要继续看 server stderr、进程退出码、系统日志和手动复现结果。

这也是我建议保留的一条排障习惯:看到 MCP Client 错误时,必须拿到 MCP Server 的原始 stderr。如果没有 stderr,就手动运行 server 命令;如果手动运行也秒退,就先不要改 MCP 配置。

4. 实际排查过程:从“谁没启动”到“为什么秒退”

这次排查可以拆成几个步骤。

4.1 先确认 Codex 主进程是不是活着

如果 Codex 主程序、app-server 或登录态本身已经不正常,那 MCP 失败只是更大问题的一部分。因此第一步是看进程和启动日志。结果显示主程序和 app-server 都在运行,普通工具调用也正常。

这一步排除了“Codex 整体启动失败”。故障范围收窄到某个 MCP server 或连接器。

4.2 再看全局配置里有没有手工 MCP 配置

很多 MCP 问题来自手写配置,例如命令名写错、参数数组少引号、环境变量没传、工作目录不存在。检查配置后发现,并没有手工配置的 mcp_servers 段。当前可用 MCP 更多来自插件和连接器。

这一步说明:不要急着修用户写的 MCP 配置,因为现场没有这类配置可修。

4.3 从日志数据库和文本日志里找 MCP 关键词

接着在 Codex 日志和本地日志库里找 mcprmcp_clientstdio_server_launcherinitialize response 等关键字。这里出现了两类信息:

第一类是 shutdown 阶段的警告:

failed to initialize MCP client during shutdown
connection closed: initialize response

这一类要谨慎解读。它发生在关闭流程中,周围还有 task cancelledRunningService dropped 之类的日志。它可能是真问题,也可能是关闭时连接被取消导致的噪声。不能只凭它下结论。

第二类更关键,是 stdio server launcher 捕获到的 server stderr:

MCP server stderr (...SkyComputerUseClient): dyld: Symbol not found
MCP server stderr (...SkyComputerUseClient): Referenced from: ...SkyComputerUseClient
MCP server stderr (...SkyComputerUseClient): Expected in: /usr/lib/swift/libswift_Concurrency.dylib

这类日志不是 MCP Client 的二手描述,而是 server 进程自己启动失败时吐出来的原始错误。优先级更高。

4.4 手动运行 server 二进制复现

为了避免“只在 Codex 启动环境里失败”的误判,最小复现就是直接运行同一个可执行文件。结果非常干净:手动启动也立刻失败,并输出同样的 dyld: Symbol not found

这一步非常关键。它证明问题不是 Codex Client 传参错,不是 MCP initialize 包错,不是连接器缓存错,也不是会话关闭竞态。这个二进制在当前系统环境下独立运行就失败。

4.5 检查二进制构建信息和系统运行环境

继续看应用的 Info.plist、文件类型和动态库依赖,可以看到几个信息:

Mach-O 64-bit executable arm64
DTSDKName: macosx26.x
DTXcode: Xcode 17.x
LSMinimumSystemVersion: 15.0

本机则是 macOS 15.x,系统 Swift runtime 位于:

/usr/lib/swift/libswift_Concurrency.dylib

错误符号是:

_swift_task_addPriorityEscalationHandler

这说明该二进制虽然标注最低系统版本可以是 macOS 15,但它在运行时引用了当前系统 Swift Concurrency runtime 中不存在的符号。也就是说,构建时使用的新 SDK / 新 Swift runtime 能看到这个符号,但当前系统自带 runtime 没有提供它。

根因不在配置,而在 Swift runtime 符号兼容性

图 3:配置层没有错,进程在动态链接阶段已经失败。

5. 根因:MCP Server 二进制和当前 macOS Swift runtime 不兼容

最终根因可以表述为:

某个 MCP Server 使用 Swift / macOS 原生二进制实现。该二进制由较新的 Xcode / macOS SDK 构建,启动时引用了当前 macOS 系统 Swift Concurrency runtime 中不存在的符号。macOS 动态链接器 dyld 在装载阶段无法解析该符号,于是直接终止进程。MCP Client 因为收不到 initialize 响应,只能报告握手失败或连接关闭。

这个结论有三个重要细节。

第一,MCP 配置并不是根因。配置错当然也会导致 MCP 启动失败,但这次配置层没有证据指向错误。直接运行 server 二进制也失败,说明配置不是必要条件。

第二,MCP Client 的报错是症状,不是根因。Client 只知道“我发起握手,但连接关闭了”。它不知道 server 是因为缺库、权限、崩溃还是主动退出。必须结合 stderr 才能定位。

第三,系统版本兼容性不能只看 LSMinimumSystemVersion。一个应用可能在 Info.plist 里写最低支持某个系统版本,但实际二进制仍然可能引用了该系统没有的 runtime 符号。真正能说明问题的是运行时的 dyld 错误和 otool / plist / 系统版本的组合证据。

6. 为什么这个问题容易误判

这个问题容易误判,主要是因为三种表象都很像“配置问题”。

6.1 MCP 的错误太抽象

handshaking with MCP server failed 是一个高层错误。它描述的是 Client 视角的失败:握手没完成。可是没完成的原因可以有几十种。配置错、命令不存在、server 输出了非 JSON 内容、server 崩溃、网络断开、HTTP 403、代理失败、证书错误、系统动态库缺失,最后都可能汇总成“握手失败”。

所以看到这类报错时,不要停在第一行。

6.2 插件型 MCP 把启动命令藏起来了

手写 MCP 配置时,启动命令就在配置文件里,很容易复制出来手动跑。但插件型 MCP 往往由插件 manifest、缓存目录、运行时包装器共同决定。用户看到的是“某个功能不可用”,不一定知道背后具体启动了哪个二进制。

这次就是从日志里的 stdio_server_launcher 才定位到具体二进制。

6.3 macOS 原生二进制错误不像 Node/Python 错误那么直观

如果是 Node 依赖缺失,通常会看到 Cannot find module。如果是 Python 包缺失,会看到 ModuleNotFoundError。但 Swift 原生二进制在动态链接阶段失败时,错误来自 dyld。它不会告诉你“请升级某个 npm 包”,而是告诉你缺某个符号。

对不常看 macOS 原生崩溃日志的人来说,Symbol not found 很容易被当成“奇怪的系统噪声”。但在这个案例里,它正是最重要的证据。

7. 如何解决:短期绕过,长期等兼容构建或升级系统

解决方案要按风险和目的分层。

修复选择取决于你是否必须使用该 MCP Server

图 4:先判断是否必须使用这个 server,再决定禁用、升级插件、升级系统或等待兼容版本。

7.1 短期:如果不需要这个工具,先禁用对应插件

如果出问题的是某个可选能力,例如本地电脑控制、浏览器自动化、某个第三方连接器,而当前任务不依赖它,最稳妥的短期处理是禁用对应插件,避免每次启动都报错。

这种做法的优点是安全、可逆、不会改系统。缺点是该插件提供的工具不可用。对很多纯代码、纯文档、纯博客任务来说,这完全可以接受。

7.2 中期:更新插件到兼容当前系统的构建

如果插件方发布了新的构建,并且明确兼容当前 macOS 版本,那么更新插件通常是最干净的解决方式。理想情况下,新构建会避免引用当前系统不存在的 Swift runtime 符号,或者把必要运行库以正确方式打包。

更新后要做两件验证:

# 1. 手动运行 server 二进制,确认不再被 dyld 秒杀
<PLUGIN_SERVER_BINARY>

# 2. 重启 Agent / 刷新 MCP 工具,确认 initialize 握手成功
codex

不要只看插件安装成功。安装成功不代表二进制能在当前系统启动。

7.3 长期:升级 macOS,或等待插件回退兼容

如果该二进制确实依赖更新系统的 Swift runtime,而你又必须使用它,那么系统升级可能是最终路径。但这不应该是第一反应,因为升级系统影响面很大,尤其是开发机器、自动化机器、带有固定工具链的环境。

更理想的长期方案是插件发布方提供一个兼容当前系统版本的构建。比如使用更保守的 SDK、正确设置 deployment target、避免引用新 runtime 符号,或者在打包中处理 Swift runtime 兼容性。

7.4 不建议:随便拷贝 Swift 动态库进系统目录

看到 Expected in: /usr/lib/swift/libswift_Concurrency.dylib 后,有人可能会想:那我找一个新版 libswift_Concurrency.dylib 拷进去不就好了?不建议这么做。

macOS 系统目录受 SIP 等机制保护,手动替换系统 Swift runtime 风险很高。即使某次能绕过去,也可能破坏其他程序、系统更新和安全边界。正确方向是换兼容构建、升级系统,或者禁用该插件。

8. 一套可复用的 MCP 启动失败排查清单

这次故障虽然发生在 Codex 和某个插件上,但方法可以复用到其他 Agent:Claude Desktop、Cursor、Cline、OpenCode、自研 Agent、各种 MCP host 都适用。

8.1 先定位失败发生在哪个 server

不要只看“某个 MCP 启动失败”。要找出具体 server 名称、启动命令、传输方式和插件来源。常见线索包括:

# 查 MCP / server / initialize 相关日志
rg -n "MCP|mcp|initialize|stdio_server|rmcp|server stderr" <LOG_DIR>

# 查配置里是否有手工 MCP server
rg -n "mcp_servers|mcpServers|server" ~/.config ~/.codex <PROJECT>

如果是插件型工具,注意找插件缓存目录和 manifest。不要假设所有 MCP 都写在一个配置文件里。

8.2 拿到 server 原始 stderr

MCP Client 的错误通常是二手信息。真正能说明问题的是 server stderr。你要找的是类似这些内容:

command not found
Permission denied
Cannot find module
ModuleNotFoundError
No such file or directory
dyld: Symbol not found
segmentation fault
HTTP 401 / 403
certificate verify failed

不同错误对应完全不同的修复方向。

8.3 复制启动命令,脱离 Agent 手动运行

如果 server 是 stdio 模式,尽量复制同一个命令,在同样用户、同样环境变量下运行。手动运行的结果能快速区分两类问题:

  1. 手动运行也失败:优先查 server 二进制、依赖、权限、系统兼容性。
  2. 手动运行成功,Agent 中失败:优先查 Agent 传参、工作目录、环境变量、沙箱、stdio 输出污染。

这一步能避免在错误层级上浪费大量时间。

8.4 区分启动失败和关闭噪声

有些日志发生在 shutdown 阶段,比如 task cancelledRunningService droppedconnection closed during shutdown。它们可能反映真实问题,也可能只是关闭时资源释放顺序导致的噪声。

判断标准很简单:同样错误是否在正常启动或工具刷新时出现?server stderr 是否有独立失败?手动运行是否复现?如果只有 shutdown 有警告,而功能平时可用,优先级就要降低。

8.5 对 macOS 原生二进制,检查这些信息

如果 stderr 出现 dyld,可以看以下信息:

# 文件架构
file <SERVER_BINARY>

# 动态库依赖
otool -L <SERVER_BINARY>

# App 元数据
plutil -p <APP>/Contents/Info.plist

# 系统版本
sw_vers
uname -m

重点不是把每个字段都背下来,而是回答几个问题:

  1. 二进制架构是否匹配当前机器?
  2. 它依赖哪些系统库或 Swift runtime?
  3. 它是不是用明显更新的 SDK / Xcode 构建?
  4. 它声明的最低系统版本和实际引用符号是否一致?
  5. 手动运行时是不是在动态链接阶段就失败?

如果答案指向 runtime 符号缺失,就不要再去改 MCP JSON。

9. 对插件作者的建议

这个问题对插件作者也有启发。MCP server 不一定是 JavaScript 或 Python。越来越多插件会包含 Rust、Go、Swift、Kotlin、.NET、C++ 等原生二进制。原生二进制性能好、能力强,但打包和兼容性责任也更重。

如果你发布 macOS MCP server,建议至少检查:

  1. deployment target 是否真的覆盖你声称支持的系统版本。
  2. CI 构建环境是否过新,导致意外引用新 runtime 符号。
  3. 是否在最低支持版本的系统上做过真实启动测试。
  4. stderr 是否能输出对用户有帮助的错误,而不是只让 Client 看到连接关闭。
  5. 插件 manifest 是否标明系统版本、架构和已知限制。
  6. 是否提供降级版本或兼容构建。

MCP host 也可以做得更好:当 server 进程 stderr 里出现 dyld: Symbol not foundCannot find modulePermission denied 这类明确错误时,最好把它们更直接地展示给用户,而不是只显示“握手失败”。

10. Q&A

Q1:看到 handshaking with MCP server failed,是不是一定是 MCP 配置错?

不是。它只说明 Client 没有完成握手。根因可能是配置,也可能是 server 崩溃、依赖缺失、系统动态库不兼容、网络失败、认证失败。必须看 server stderr 和手动复现。

Q2:为什么 Client 不直接告诉我 dyld 错误?

Client 只负责通过 transport 和 server 通信。server 如果在启动阶段就被操作系统杀掉,Client 只能观察到连接关闭。是否能展示 stderr,取决于 host 是否捕获并上报 server 进程的 stderr。

Q3:dyld: Symbol not found 是什么?

这是 macOS 动态链接器报告的错误。程序启动时需要解析依赖库里的函数和符号。如果二进制引用了某个符号,但当前系统库里没有这个符号,程序就无法启动。

Q4:升级 Xcode 或 Command Line Tools 能修吗?

不一定。这里缺的是系统 /usr/lib/swift 里的 runtime 符号,而不是编译器命令本身。升级命令行工具可能改变开发环境,但不一定改变系统运行时。真正的修复通常是换兼容构建、升级 macOS,或等待插件发布方修复。

Q5:能不能手动替换 /usr/lib/swift/libswift_Concurrency.dylib

不建议。系统 Swift runtime 属于系统组件,手动替换风险很高,也可能被系统保护机制阻止。不要为了一个插件破坏系统运行库。

Q6:为什么同一个 Agent 里有些 MCP 能用,有些不能?

因为每个 MCP server 的实现和依赖不同。一个可能是 HTTP 远端服务,一个可能是 Node 包,一个可能是 Python 脚本,一个可能是 Swift 原生 app。某个 server 的 runtime 不兼容,不代表整个 MCP 子系统都坏了。

Q7:如果工具缓存还能用,要不要管这个错误?

如果错误只影响可选工具,并且当前任务不依赖它,可以暂时禁用或忽略。但如果你需要该工具,或者每次启动都刷屏,就应该定位具体 server 并处理。缓存可用不等于 server 健康。

Q8:这类问题怎么写进团队 runbook?

可以写成四步:先确定具体 server;再看 server stderr;然后手动运行 server 命令;最后根据错误类型分流到配置、依赖、权限、网络或系统 runtime。不要把所有 MCP 启动失败都归为“重装插件”。

11. 最后总结

这次故障的教训很简单:MCP 是一层协议和连接机制,不是所有问题的根因所在地。Client 报握手失败,只是说“我没等到对方回答”。真正的问题可能发生在 server 启动命令、依赖、权限、网络,也可能像这次一样,发生在 macOS 动态链接器解析 Swift runtime 符号的瞬间。

一个高质量排障过程,不是看到错误关键词就改配置,而是沿着证据往下追:Client 日志、server stderr、手动复现、二进制信息、系统 runtime。只要能把失败层级定位清楚,修复方案通常就会变得直接:禁用可选插件、升级插件、等待兼容构建,或者在确实必要时升级系统。

对 Agent 用户来说,这也是一个提醒:工具越多,链路越长,越需要把“症状”和“根因”分开。MCP 让 Agent 能力更强,但 MCP Server 仍然只是一个普通进程。普通进程会遇到普通问题:命令不存在、依赖缺失、权限不足、动态库不兼容。最终能救你的,仍然是日志、复现和分层分析。

来源说明

本文来自一次本地 Codex / MCP 启动故障排查的整理。为保护隐私,文中所有主机地址、用户名、绝对路径、会话 ID、进程号、私有仓库和环境细节均已泛化或省略;保留的只有可公开复用的错误类型、诊断方法和技术结论。