1. 为什么 MCP 消息格式总在 Cline 里报错
MCP(Model Context Protocol)模型上下文协议,说白了就是让 AI 客户端和外部工具服务端用一套标准话术对话的约定。它规定了客户端能问什么、服务端能答什么、哪些消息不需要回答。适合谁?适合正在用 Cline、Claude Code 这类工具接自定义 MCP 服务端,却被Invalid request、id must not be null、method not found卡住的开发者。
我见过太多人第一次写 MCP 服务端,直接把普通 HTTP 接口那套{"code":0,"data":{}}搬过来,结果 Cline 一连就断。原因很简单:MCP 底层用的是 JSON-RPC 2.0,消息结构有硬性约束,字段名、id 规则、result 与 error 互斥,一条不符合就整条会话失败。更隐蔽的是能力协商——如果服务端在initialize阶段没声明tools能力,客户端根本不会去调tools/list,你后面写的工具函数永远收不到请求。
这篇是进阶篇 2,聚焦两件事:三类消息(请求、响应、通知)到底怎么构造和解析,以及能力协商字段清单怎么填。场景落在 Cline MCP 上,我会给出可复制的服务端配置片段,并用 TaoToken 统一 Key 通道发起一次真实调用,把预期返回结构贴出来。你跟着做,能跑通一条完整的initialize → tools/list → tools/call链路。
先明确一个检索词:MCP JSON-RPC 消息格式与能力协商,是这篇的核心。你如果搜的是「MCP 请求响应通知区别」「MCP initialize 能力字段」,方向一致。
三类消息的边界,用一句话记:请求有 id 且要回,响应有 id 且只带 result 或 error 之一,通知没有 id 也不回。听起来简单,但实际写代码时,最容易错的是把通知也塞了 id,或者响应里 result 和 error 同时出现。Cline 对这两点零容忍。
下面从消息结构逐层拆,再进配置和验证。每一步都给完整字段,不省略。
2. TaoToken 统一 Key 通道前置准备
在讲配置之前,先把调用通道说清楚。MCP 服务端本身不负责模型推理,它只暴露工具;真正要跑通「模型决定调哪个工具」这一步,需要一个能访问大模型的通道。TaoToken 在这里的角色是统一 Key 通道:你用同一个 Key,就能在 Cline、Claude Code、Codex 这些客户端里发起模型请求,不用为每个客户端单独配一套凭证。
官网入口:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
API 地址:https://taotoken.net/api
你需要准备三样东西,缺一不可:
第一,一个可用的 API Key。到控制台创建,路径是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,在 API Keys 页面生成。生成后立刻复制,页面刷新就看不到了。这一步对应 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
第二,确认你要用的 Model ID。不同客户端对模型名的写法略有差异,但统一通道下,你填的是同一套标识。可以在模型对话页先试一次,地址是 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,发一条消息看返回是否正常,确认 Key 和模型都对。
第三,MCP 服务端的运行环境。这篇用 Node.js 写一个最小服务端,通过 stdio 和 Cline 通信。你本地要有 Node 18 以上。Cline 的 MCP 配置走的是客户端配置文件,不是环境变量,这点和普通 CLI 不同。
为什么强调「统一 Key」?因为 MCP 场景下,客户端既要连模型通道,又要连 MCP 服务端,两套配置容易混。TaoToken 把模型通道收敛成一个 Base URL 加一个 Key,你只需要在客户端里填一次,MCP 服务端那边专心处理 JSON-RPC 就行,职责分离,排障时能快速定位是模型侧还是协议侧的问题。
如果你打算长期跑编码类 Agent,可以看 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,字段说明以文档为准。
前置准备做完,下面进真正的配置。记住三件套:Base URL、Key、Model ID,后面每一处配置都会围绕它们展开。
3. 可复制的 MCP 服务端配置与消息构造
这一节是全文技术核心,给完整可复制的片段。先看 Cline 侧的 MCP 配置,再看服务端消息构造。
Cline 的 MCP 配置通常写在客户端的 settings 文件里,结构是 JSON。下面这段可以直接改路径后用:
{ "mcpServers": { "taotoken-demo": { "command": "node", "args": ["/Users/yourname/mcp-demo/server.js"], "env": { "TAOTOKEN_BASE_URL": "https://taotoken.net/api", "TAOTOKEN_API_KEY": "sk-你的Key", "TAOTOKEN_MODEL_ID": "你的ModelID" } } } }注意三点:command是启动命令,args是脚本绝对路径,env里放三件套。Cline 启动时会用 stdio 拉起这个进程,然后开始 JSON-RPC 握手。
服务端server.js的最小实现,处理三类消息。先看请求解析:
// server.js const readline = require('readline'); const rl = readline.createInterface({ input: process.stdin }); function send(msg) { process.stdout.write(JSON.stringify(msg) + '\n'); } rl.on('line', (line) => { let req; try { req = JSON.parse(line); } catch (e) { // 解析失败也不能带 id,因为不知道对应哪个请求 send({ jsonrpc: '2.0', error: { code: -32700, message: 'Parse error' } }); return; } // 通知:没有 id,不回复 if (req.id === undefined) { if (req.method === 'notifications/initialized') { // 客户端告知初始化完成,这里只记录,不回 return; } return; } // 请求:有 id,必须回 if (req.method === 'initialize') { send({ jsonrpc: '2.0', id: req.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: { listChanged: true } }, serverInfo: { name: 'taotoken-demo', version: '1.0.0' } } }); return; } if (req.method === 'tools/list') { send({ jsonrpc: '2.0', id: req.id, result: { tools: [ { name: 'echo_text', description: '回显输入文本', inputSchema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] } } ] } }); return; } if (req.method === 'tools/call') { const text = req.params?.arguments?.text ?? ''; send({ jsonrpc: '2.0', id: req.id, result: { content: [{ type: 'text', text: `echo: ${text}` }] } }); return; } // 未知方法,回 error,且不能同时带 result send({ jsonrpc: '2.0', id: req.id, error: { code: -32601, message: 'Method not found' } }); });这段代码把三类消息的规则全落实了:通知直接 return 不回复;请求按 method 分支回 result;未知方法回 error。initialize的返回里,capabilities.tools.listChanged就是能力协商字段,声明了支持工具列表变更通知。
能力协商字段清单,对照填:
| 类别 | 能力 | 说明 |
|---|---|---|
| Client | roots | 提供文件系统根目录 |
| Client | sampling | 支持 LLM 采样请求 |
| Client | experimental | 非标准实验功能 |
| Server | prompts | 提供提示模板 |
| Server | resources | 提供可读资源 |
| Server | tools | 暴露可调用工具 |
| Server | logging | 发送结构化日志 |
| Server | experimental | 非标准实验功能 |
子能力里,listChanged适用于 prompts、resources、tools,表示列表变化时发通知;subscribe只适用于 resources,表示支持订阅单项变更。你如果没实现变更通知,就别声明listChanged: true,否则客户端等通知等不到,会超时。
消息构造的硬规则再强调一遍:请求 id 不能为 null,同一会话不能重复;响应必须带与请求相同的 id,result 和 error 二选一;通知不能有 id。这三条是 JSON-RPC 2.0 在 MCP 里的落地约束,写错一条,Cline 直接断连。
配置和代码都齐了,下一节验证。
4. 验证请求与成功返回结构
验证分两步:先确认 MCP 服务端能被 Cline 拉起并完成握手,再确认通过 TaoToken 通道发起的模型调用能触发工具。
第一步,把server.js放到配置里的路径,重启 Cline。打开 MCP 面板,应该看到taotoken-demo状态变成已连接。如果没连上,看 Cline 的 MCP 日志,通常会打印 stderr。
第二步,手动模拟一次握手,确认消息格式对。在终端里跑:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | node server.js预期返回:
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{"listChanged":true}},"serverInfo":{"name":"taotoken-demo","version":"1.0.0"}}}看到capabilities.tools就说明能力协商字段生效了。接着测tools/list:
echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' | node server.js预期返回里result.tools是数组,含echo_text。再测tools/call:
echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo_text","arguments":{"text":"hello mcp"}}}' | node server.js预期返回:
{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"echo: hello mcp"}]}}第三步,走 TaoToken 通道做端到端验证。在 Cline 对话框里输入「用 echo_text 工具回显 hello」,模型会先请求tools/list,再发tools/call。你观察 MCP 日志,应该看到两条请求依次进来,id 递增,返回结构正确。模型侧收到content后,会把echo: hello mcp展示出来。
这一步能跑通,说明三件事同时成立:JSON-RPC 消息格式正确、能力协商声明正确、TaoToken 通道的 Base URL 和 Key 配置正确。任何一环错,都会在日志里留下痕迹。
如果你在模型对话页单独测通道,地址是 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,发一条普通消息确认返回正常,再回 Cline 测工具调用,能更快定位问题在通道还是在协议。
验证通过后,返回结构里的content数组是标准形态,type: "text"是最常用的一种。你后面扩展工具时,返回结构保持一致,客户端就能统一解析。
5. 本篇常见错误排查
这一节对照真实报错,逐个拆。
报错一:id must not be null或Invalid request。原因通常是请求里 id 写成了 null,或者干脆没写 id 却当成请求发。JSON-RPC 2.0 基础规范允许 id 为 null,但 MCP 明确禁止。检查你的请求构造,id 用递增整数或字符串,别用 null。通知才不带 id,别混。
报错二:local proxy failed或连接被拒。这个多半出在通道配置。检查 Cline 的 MCP 配置里TAOTOKEN_BASE_URL是否写成https://taotoken.net/api,注意结尾没有多余斜杠。Key 是否复制完整,有没有前后空格。Model ID 是否和你在模型对话页验证过的一致。三件套任一错,模型侧请求就发不出去,表现为代理失败。
报错三:reading 'choices'或返回结构解析失败。这是模型侧返回不符合预期。常见原因是 Model ID 填错,或者通道返回的是错误对象而你按成功结构解析。先在模型对话页确认返回正常,再回客户端。如果通道返回里带error字段,先处理错误,别硬读choices。
报错四:OAuth相关或鉴权失败。检查 Key 是否过期、是否在控制台被删除。重新生成一个,更新到配置里,重启 Cline。注意 Key 只在生成时可见,别用旧截图里的。
报错五:Method not found。服务端没实现对应 method。对照你的server.js,确认initialize、tools/list、tools/call都有分支。Cline 握手时会先发initialize,再发notifications/initialized(通知,无 id),然后才tools/list。少一个分支就报这个。
报错六:响应里 result 和 error 同时出现。这是格式违规。检查你的send调用,确保每个分支只走 result 或只走 error。未知方法走 error,正常走 result,别在同一个响应里都塞。
排查顺序建议:先看 Cline MCP 日志确认握手到哪一步,再用终端 echo 模拟请求确认服务端单独能跑,最后查通道三件套。分层定位,比一上来就改代码快。
如果你用的是 Claude Code 接 Anthropic 风格配置,参考 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,字段名和 Cline 略有差异,但三件套逻辑一致。Claude Code 相关入口在 https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
6. 继续把 MCP 链路跑稳
消息格式和能力协商这两块吃透后,你扩展 MCP 服务端会顺很多。我的经验是:先把initialize的返回字段写全,尤其是capabilities,客户端靠它决定后续发什么请求;再保证三类消息的 id 规则不破;最后才去加业务工具。顺序反了,排障会很痛苦。
下一步你可以试着自己加一个resources能力,声明subscribe: true,然后实现资源变更通知,观察 Cline 是否响应。这一步能帮你彻底理解通知和请求的区别。
需要长期跑编码 Agent 的,Coding Plan 入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。接入文档和字段细节以 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 为准。Key 管理在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
把server.js里的echo_text换成你真正要暴露的工具,inputSchema 写清楚,返回结构保持content数组,链路就通了。