最近在自建 MCP 客户端时,最容易被一条看似最简单的 JSON-RPC 心跳卡住:ping 发出去,等半天没有回包。MCP 2025-03-26 版在「ping」一节把话说得很清楚,但要让 Codex 帮你逐行核对这段逻辑,先打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 创建 API Key,并把 Codex 的模型通道指向 TaoToken。很多朋友的 ping 超时并不是网络断了,而是请求格式、响应格式或生命周期顺序与规范不一致:ping 请求不能带 params,接收方必须立即返回一个空 result,超时之后发送方可以终止连接或重连。Codex 在这里的作用不是替你去连生产库,而是读取你本地导出的 MCP 客户端代码和 stdout 日志,对照规范指出字段差异。下面按 2025-03-26 版规范的章节顺序,把 ping 消息、初始化生命周期、stdio 与 Streamable HTTP 传输、错误码和排障拆开讲。你只需要在本地跑最小调试会话,把原始 JSON 行贴回对话,就能让 Codex 给出可落地的修改建议。
1. MCP 2025-03-26 的 ping 到底长什么样
1.1 ping 请求只有 jsonrpc、id、method 三个字段
规范里对 ping 的定义非常克制。一个合法的 ping 请求看起来就是这样:
{ "jsonrpc": "2.0", "id": "123", "method": "ping" }它没有params,也不应该出现params: {}。id可以是字符串或数字,但不能为空,并且在同一会话中不能和之前用过的请求 id 重复。很多自建客户端在这里会犯两个错误:一是把 ping 写成通知,也就是去掉id只留method;二是为了“看起来完整”硬塞一个空对象当参数。前者会让接收方按通知处理,通知没有 id,接收方不能响应,你自然等不到任何回包;后者在严格实现里可能被当成无效参数,返回-32602。
提示:ping 是请求,不是通知。请求必须有 id,通知不能有 id。先检查这一条,能省掉一半的“没响应”排查。
1.2 合规响应是 result 为空对象,不是业务数据
接收方收到 ping 之后,规范要求立即返回一个空响应。格式如下:
{ "jsonrpc": "2.0", "id": "123", "result": {} }注意两个细节。第一,id必须和请求完全一致,字符串"123"和数字123在严格比较下不是一回事。第二,result必须是空对象,不能塞{"status": "ok"}或{"pong": true}。虽然那些 JSON-RPC 层面仍然合法,但不满足 ping 的空响应约定。如果你的客户端只判断“收到了 result 就算成功”,可能会把业务响应误当成 ping 回包,掩盖真正的协议问题。Codex 在核对时,你可以让它重点检查响应构造分支里有没有对空对象做断言。
1.3 超时不是异常,而是协议允许的终止动作
规范没有要求发送方无限等待。如果在合理超时期限内没有收到响应,发送方可以考虑连接过期、终止连接,或者尝试重新连接。这里有几个实现上的分寸:ping 频率应该可配置,超时要适配当前网络环境,不应无节制地发 ping 增加开销;多个 ping 连续失败后,可以触发连接复位;每次失败都应该留下诊断日志,方便后面把原始 JSON 行贴给 Codex 分析。
很多客户端把超时当成普通错误抛给上层,导致整个 MCP 会话直接崩掉。更稳的做法是:先标记该请求超时,发送notifications/cancelled停止等待,再根据连续失败次数决定是否关闭连接。规范里也提到,接收方可以忽略取消通知,如果请求已经处理完成或无法取消,所以发送方要能容忍“取消通知没有回音”这件事。
2. 自建 MCP 客户端 ping 超时,Codex 能检查哪几层
2.1 先拿 Key,再把 Codex 的 config.toml 指向 TaoToken
你不需要先把整个 MCP 服务器改完。先用 TaoToken 把 Codex 的模型通道配通,让它能读取你贴过去的代码片段和日志。打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 注册并创建 API Key,然后在模型广场里确认你要用的模型 ID。Key 用占位符YOUR_API_KEY,不要写死在仓库里。
Codex 的配置文件是~/.codex/config.toml,不是 Claude Code 的settings.json,两者不要混用。一个可参考的配置片段如下:
model = "YOUR_MODEL_ID" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY"环境变量在本地 shell 里设置:
export TAOTOKEN_API_KEY=YOUR_API_KEY模型 ID 以 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 模型广场当时列表为准,不要凭记忆写一个带日期后缀的名字。base_url填https://taotoken.net/api,末尾不要加/v1。这两点确认完,再启动 Codex。
2.2 给 Codex 的提示词要限定“只读代码和日志”
Codex 不能替你执行 MCP 服务器,也不能直连你的生产库。它适合做的是:读取你本地导出的代码、配置、日志文本,对照规范给出差异清单。提示词可以这样写:
下面是我的 MCP 客户端中发送 ping 和处理超时的代码,以及一段 stdout 日志。请对照 MCP 2025-03-26 版 ping 一节检查: 1. ping 请求是否只包含 jsonrpc、id、method,是否误带 params; 2. 响应是否为 result: {},id 是否严格匹配; 3. 超时后是否发送 notifications/cancelled 并停止等待; 4. 初始化前后发送 ping 的时机是否合规。 只输出差异和本地验证步骤,不要尝试执行任何外部命令,不要连接任何数据库或生产系统。这样 Codex 的输出会收敛在协议对照上,而不是跑去编造一个“自动修复并部署”的流程。你拿到建议后,仍然在本地修改代码、跑调试会话、抓新的 JSON 行,再贴回对话。
2.3 Codex 负责分析,MCP ping 仍然发生在你的客户端与服务器之间
这里要分清两条通道:MCP ping 是 MCP 客户端和 MCP 服务器之间的 JSON-RPC 心跳;TaoToken 是 Codex 背后的模型 API 通道。Codex 不会替你去 ping 那个 MCP 服务器,它只是在你把日志贴过去之后,帮你判断日志里的 ping 消息是否符合规范。把这两个概念混在一起,很容易写出“让 Codex 直接连上 MCP 服务器执行 ping”的错误方案。正确的桥是:本地跑 MCP 调试会话,导出原始消息,交给 Codex 分析,你在本地改代码再验证。
3. 对照 2025-03-26 规范逐行查:ping、生命周期、批处理、错误码
3.1 初始化阶段之前只允许 ping 和日志类消息
规范对初始化顺序有明确约束:在服务器响应初始化请求之前,客户端不应该发送 ping 以外的请求;在收到初始化完成通知之前,服务器不应该发送 ping 和日志以外的请求。如果你在initialize还没完成时就发了tools/list或resources/read,服务器可能直接忽略,日志上看起来却像“ping 超时”。排查时先把消息按时间顺序排开,确认initialize请求、initialize响应、notifications/initialized通知三者的先后关系。
一个常见的自建客户端错误是:连接建立后立刻起一个定时器发 ping,但initialize请求还在路上。此时服务器可能还没完成能力协商,ping 被排队或丢弃。更稳妥的做法是把 ping 定时器绑在notifications/initialized之后启动,并在初始化阶段单独设置更短的握手超时。
3.2 JSON-RPC 批处理与 ping:初始化请求不能进批
2025-03-26 版支持 JSON-RPC 批处理,发送方可以把多个请求或通知放进数组。MCP 实现可能支持发送端批处理,但必须支持接收端批处理。这里有一条容易被忽略的规则:初始化请求不能成为 JSON-RPC 批处理的一部分。也就是说,你不能把initialize和一条 ping 打包成数组发出去。
如果你确实要把 ping 放进批处理,先确认服务器在初始化完成后能正确解析数组,并且返回的响应数组里每个响应都带对应的 id。ping 本身很简单,但批处理会引入顺序、部分失败和取消通知的复杂度。排障阶段建议先让 ping 单独成请求,等基础路径稳定后再考虑批处理。
3.3 超时、取消通知与常见错误码
规范在超时部分提到:当请求在超时期间内没有收到成功或错误响应时,发送方应该为该请求发出取消通知,并停止等待响应。取消通知的method是notifications/cancelled,参数里带requestId和可选reason。它仍然是通知,没有 id,接收方不应该为它返回响应。
ping 本身不应该返回错误,但如果 method 拼错、参数非法或服务器不支持某些能力,可能会看到这些错误码:
| 错误码 | 含义 | 与 ping 排查的关系 |
|---|---|---|
-32601 | 方法未找到 | 可能把ping写成了Ping或服务器未实现该方法 |
-32602 | 无效参数 | ping 被误加了 params,或参数结构不符合方法要求 |
-32603 | 内部错误 | 服务器处理 ping 时内部异常,需要看服务器日志 |
Codex 分析日志时,可以让它优先找-32601和-32602,这两个码往往直接指向消息格式问题。
4. 在本地把 ping 调试会话跑起来
4.1 stdio 传输最小复现:发一条 ping,断言空 result
stdio 传输下,MCP 服务器作为子进程启动,客户端从 stdin 写 JSON-RPC 消息,从 stdout 读响应。消息由换行符分隔,不能包含嵌入换行,编码必须是 UTF-8。下面是一段 Node.js 最小复现脚本,重点看它如何断言 ping 响应:
const { spawn } = require('node:child_process'); const server = spawn('node', ['your-mcp-server.js'], { stdio: ['pipe', 'pipe', 'pipe'] }); let buffer = ''; server.stdout.on('data', (chunk) => { buffer += chunk.toString('utf8'); const lines = buffer.split('\n'); buffer = lines.pop(); for (const line of lines) { if (!line.trim()) continue; const msg = JSON.parse(line); console.log('收到消息:', JSON.stringify(msg)); if (msg.id === 'ping-1') { const isEmptyResult = msg.result && typeof msg.result === 'object' && Object.keys(msg.result).length === 0; if (isEmptyResult) { console.log('ping 响应合规:result 为空对象'); } else { console.log('ping 响应不符合规范:', JSON.stringify(msg)); } server.stdin.end(); } } }); server.stderr.on('data', (chunk) => { console.error('服务器 stderr:', chunk.toString('utf8')); }); const ping = { jsonrpc: '2.0', id: 'ping-1', method: 'ping' }; server.stdin.write(JSON.stringify(ping) + '\n'); setTimeout(() => { console.log('超时:未收到 ping 响应,按规范可以终止连接或重连'); server.kill('SIGTERM'); }, 5000);注意脚本里没有往 ping 里加params,也没有把响应结果当成业务数据。服务器如果往 stdout 打印日志,会破坏 JSON 行解析,所以调试时把日志写到 stderr,或者单独重定向。
4.2 Streamable HTTP 下 ping 的 POST 与 Accept 头
如果你的 MCP 服务器走 Streamable HTTP,ping 是一个新的 HTTP POST 请求,发到 MCP 端点。客户端必须带Accept头,列出application/json和text/event-stream。如果输入只有响应或通知,服务器接受时返回202 Accepted且不带正文;如果输入包含请求,服务器可能返回 SSE 流,也可能返回 JSON 对象。ping 是请求,所以你要准备好解析这两种 Content-Type。
这里再强调一次:不要把 MCP ping 发到https://taotoken.net/api。这个地址是 Codex 调用模型的 API Base URL,不是你的 MCP 服务器端点。MCP 端点由你自己的服务器决定,比如https://your-mcp.example.com/mcp。两者混用会导致 404 或协议不匹配。
4.3 把原始日志贴回 Codex,让它按规范找差异
日志至少保留这些字段:时间戳、传输方式、原始 JSON 行、请求 id、响应 id、错误码。脱敏之后贴给 Codex,并要求它输出一张对照表:规范要求、你的实现、差异、本地验证命令。不要让 Codex 执行命令,也不要让它连接你的数据库。它只做文本分析,真正的执行仍然在你本地。
如果你的 Codex 连不上模型通道,先回到 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 检查 Key、模型 ID 和用量。配置里base_url写https://taotoken.net/api,不要加/v1,也不要在这条 URL 后面拼任何查询参数。
5. 排障:ping 没响应最常见的几个原因
5.1 请求里混进了 params 或 method 拼错
ping 请求没有参数。如果你写成{"jsonrpc":"2.0","id":1,"method":"ping","params":{}},严格服务器可能返回-32602。method 是大小写敏感的,ping和Ping不是同一个方法。让 Codex 检查消息构造函数,确认 ping 分支没有复用了其他方法的参数模板。
5.2 响应 id 对不上、result 里塞了业务数据
响应必须带与请求相同的 id。如果 id 类型不一致,或者响应里result不是空对象,客户端的匹配逻辑就可能走到错误分支。有的服务器为了“方便”,在 ping 响应里返回{"result":{"ok":true}},这不会让 JSON-RPC 解析失败,但不符合 ping 的空响应约定。Codex 可以帮你把响应分支里的所有 return 点列出来,逐个核对。
5.3 把通知当 ping,或超时后没有取消
没有 id 的{"jsonrpc":"2.0","method":"ping"}是通知,接收方不能响应。如果你用这种方式发心跳,永远等不到回包。超时之后,规范建议发送notifications/cancelled并停止等待。很多客户端只是setTimeout后重连,没有取消通知,可能导致旧请求和新连接交错,日志更乱。
5.4 初始化前抢跑、批处理位置不对
初始化请求必须是双方第一次交互,并且不能放进批处理。客户端在服务器响应初始化之前只应发 ping;服务器在收到初始化完成通知之前只应发 ping 和日志。顺序错了,消息可能被忽略。把日志按时间轴画出来,确认没有抢跑。
5.5 stdio 换行、编码、stderr 污染
stdio 传输要求每条消息由换行符分隔,不能包含嵌入换行,UTF-8 编码。服务器不能往 stdout 写非 MCP 消息。如果服务器把调试日志打到 stdout,客户端解析 JSON 失败,就会表现为“ping 没响应”。检查你的进程启动配置,把 stderr 单独接出来,不要合并到 stdout。
5.6 模型通道没通,Codex 分析中断
如果 Codex 的base_url、环境变量或模型 ID 不对,它可能根本没帮你分析日志。打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 看控制台用量,确认这次请求是否记上账。如果没记上,先检查TAOTOKEN_API_KEY是否生效、model_provider是否写对、base_url是否误加了/v1。模型通道通了,再回来排查 MCP ping。
6. 跑通之后去模型对话和控制台对一下这次验证
6.1 用同一把 Key 发一条测试消息
配置保存后,先在 TaoToken 模型对话 里用同一把 Key 发一条测试消息,确认模型 ID 和 Base URL 没填错。如果这里能正常回复,说明 Codex 的模型通道没有明显问题。接着再跑本地 MCP ping 调试脚本,把 stdout 里的原始 JSON 行贴回 Codex。
如果要长期用 Codex 分析 MCP 日志和代码,可以打开 Coding Plan 看套餐是否够用。Key 在 控制台 API Keys 创建,创建时顺手确认模型广场里的模型 ID。Claude Code 环境变量对照见 接入文档,如果你同时维护多个 AI 编程工具,可以把 Base URL 和 Key 的写法做成一份本地备忘。
6.2 看用量确认这次调用记上账
回到 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 的控制台看用量,确认这次让 Codex 分析 MCP ping 日志的请求有没有记上。如果用量没有变化,优先检查 Codex 是否真的走了taotokenprovider,而不是还在用旧环境变量。用量对得上,再排查 MCP 客户端本身。
6.3 把最小 ping 用例沉淀成回归脚本
MCP 规范会继续演进,你的 MCP 服务器和客户端也会改。把前面那段最小 ping 脚本留下来,固定请求 id,断言响应result为空对象,超时后发送取消通知。每次升级 MCP SDK 或切换传输方式,先跑一遍这个脚本,再把差异日志丢给 Codex 对照规范。这样 ping 超时就不再是玄学问题,而是一条可以复现、可以回归的检查项。