☰
MCP 中 JSON-RPC 请求完整详解:从 stdio 到 TaoToken 的 Request 与 Notification 实践
2026/10/2 12:28:08 网站建设 项目流程

1. 为什么 stdio 下的 JSON-RPC 总在“最后一公里”翻车

MCP 全称 Model Context Protocol,你可以把它理解成“让大模型调用外部工具和资源的一套标准插头”。它底层不玩花活,通信协议就是 JSON-RPC 2.0,而本地场景里最常用的传输方式是 stdio——也就是父进程和子进程之间用标准输入输出管道对话。听起来简单,但真正动手写一个 MCP Server 或者调试一个第三方 Server 时,十有八九会卡在“请求发出去了,响应没回来”或者“响应回来了,但解析报错”上。

问题往往不在业务逻辑,而在 JSON-RPC 的报文格式和 stdio 的传输规则没对齐。Request 必须带id,Notification 绝对不能带id;stdout 只能吐 JSON-RPC 报文,任何一句print("debug")都会把整条管道污染成不可解析的垃圾;每条报文必须单行、末尾换行,JSON 内部不能有裸换行。这些规则单独看都懂,合在一起写代码时就容易漏。

这篇文章面向三类人:正在写 MCP Server 的后端开发、用 Cline/Claude Code 这类客户端接自定义工具的工程师、以及想搞明白“为什么我的 MCP 工具列表刷不出来”的排障选手。我会从 stdio 传输的完整生命周期讲起,把 Request 和 Notification 的构造、发送、匹配、处理拆开,再结合 TaoToken 的统一 API 通道(https://taotoken.net/api)演示怎么把模型调用和 MCP 工具链串起来。你跟着做,能拿到可复制的 JSON-RPC 片段和一套本地验证请求-响应链路的操作步骤。

先记住一个核心检索词:MCP JSON-RPC stdio Request Notification 完整生命周期。下面所有内容都围绕它展开。

2. TaoToken 前置:统一 Key 与 API 通道在 MCP 链路里的位置

在讲报文之前,得先把“模型从哪来”这件事说清楚。MCP 本身只负责工具调用协议,它不提供模型。你的 MCP Client(比如 Claude Code、Cline、或者自己写的宿主程序)需要一个大模型后端来决策“什么时候调哪个工具”。TaoToken 在这里的角色是统一 Key 和 API 通道:你拿一个 Key,就能通过兼容接口访问多家模型,不用为每个模型单独配一套鉴权和 Base URL。

对 MCP 调试来说,这带来两个实际好处。第一,你的 MCP Client 配置里只需要维护一份 API Key 和一个 Base URL,减少变量,排障时能快速排除“是不是 Key 配错了”。第二,TaoToken 的接口兼容主流协议,Claude Code、Cline、Codex 这类工具可以直接把 Base URL 指过来,模型对话和工具调用走同一条通道,日志集中,出问题好定位。

你需要提前准备的东西不多:一个 TaoToken 的 API Key(在控制台创建,地址是 https://taotoken.net/console),以及确认你的 MCP Client 支持自定义 Base URL。Base URL 填https://taotoken.net/api,注意这个地址不带任何查询参数。Key 的格式通常是sk-开头的一串字符,创建后只显示一次,记得存好。

这里要强调一个边界:TaoToken 是合法的 API 通道服务,不是所谓“中转”或任何灰色设施。你用它就是正常调用模型接口,和直接用官方 API 没有本质区别,只是入口统一了。MCP 的 stdio 传输发生在你本地进程之间,和 TaoToken 的网络请求是两层,不要混在一起理解。模型请求走 HTTPS 到 TaoToken,工具调用走 stdio 到本地 MCP Server,两者通过 MCP Client 这个宿主程序协调。

如果你用的是 Claude Code 这类带 MCP 支持的编码工具,配置入口一般在 settings 或专门的 MCP 配置文件里。下面第三节我会给出可复制的配置片段,包括 Base URL、Key 和 Model ID 三件套,以及 MCP Server 的 stdio 启动参数。

3. 可复制配置:JSON-RPC 报文、MCP Server 声明与 settings 片段

这一节是全文的操作核心。我会分三块给配置:第一块是 JSON-RPC 报文本身(Request 和 Notification 各给可复制片段),第二块是 MCP Server 在客户端里的声明(以 Claude Code 的 settings 风格为例),第三块是模型通道的配置(Base URL + Key + Model ID 三件套)。

先看 JSON-RPC 请求。MCP 初始化握手是连接建立后的第一条 Request,必须带id,服务端必须回响应。你可以直接复制这段:

{ "jsonrpc": "2.0", "id": 0, "method": "initialize", "params": { "protocolVersion": "0.1.0", "clientInfo": { "name": "my-mcp-client", "version": "1.0.0" }, "capabilities": {} } }

字段含义:jsonrpc固定"2.0";id是客户端自定义编号,用来匹配后续响应,初始化用 0 是常见约定;method是initialize;params里协商协议版本和客户端信息。服务端成功响应会带回同样的id:

{ "jsonrpc": "2.0", "id": 0, "result": { "protocolVersion": "0.1.0", "serverInfo": { "name": "my-mcp-server", "version": "0.1.0" }, "capabilities": { "tools": {} } } }

握手完成后,列出工具用tools/list,调用工具用tools/call。调用工具的 Request 长这样:

{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "list_directory", "arguments": { "path": "./docs" } } }

注意id必须唯一。如果你并发发多个请求,靠id区分哪个响应对应哪个请求。响应里的id会原样带回,这是匹配的唯一依据。

再看 Notification。它和 Request 的唯一区别就是没有id,服务端收到后不需要回复。典型用途是日志推送:

{ "jsonrpc": "2.0", "method": "logging/message", "params": { "level": "info", "message": "开始扫描本地文件夹" } }

如果你给 Notification 加了id,它就不再是 Notification,而变成了一个需要响应的 Request,服务端不回你就会一直等,这是常见坑。

接下来是 MCP Server 在客户端里的声明。以 Claude Code 风格的 settings 为例,MCP Server 配置通常放在~/.claude/settings.json或项目级.mcp.json里。一个 stdio 类型的 Server 声明如下:

{ "mcpServers": { "my-local-tools": { "command": "node", "args": ["/absolute/path/to/mcp-server.js"], "env": { "MCP_LOG_LEVEL": "info" } } } }

command是启动 Server 的可执行文件,args是参数,env是环境变量。关键点:Server 进程的 stdout 只能输出 JSON-RPC 报文,日志必须走 stderr。你在 Server 代码里用console.error而不是console.log,就是这个原因。

最后是模型通道配置。Claude Code 这类工具支持自定义 Base URL 和 Key,配置片段如下(路径以实际工具为准,这里给的是通用结构):

{ "apiBaseUrl": "https://taotoken.net/api", "apiKey": "sk-你的Key", "model": "claude-sonnet-4-20250514" }

三件套齐了:Base URL 是https://taotoken.net/api,Key 在控制台创建,Model ID 按你实际使用的模型填。如果你用 Codex 的auth.json风格,结构类似,把 Base URL 和 Key 填进对应字段即可。Cline 的 MCP 配置则在扩展设置里,Base URL 同样指向 TaoToken 的 API 地址。

配置写完后,别急着跑复杂工具,先用一个最小 Server 验证链路。下一节给验证步骤。

4. 验证请求-响应链路:用 stdio 本地跑通一次完整往返

验证的目标很简单:启动一个 MCP Server 子进程,通过 stdin 发一条initializeRequest,从 stdout 读到带相同id的响应。跑通这一步,后面的tools/list和tools/call都是同一套逻辑。

先写一个最小的 MCP Server,用 Node.js 举例,文件叫mcp-server.js:

process.stdin.setEncoding('utf8'); let buffer = ''; process.stdin.on('data', (chunk) => { buffer += chunk; let newlineIndex; while ((newlineIndex = buffer.indexOf('\n')) !== -1) { const line = buffer.slice(0, newlineIndex).trim(); buffer = buffer.slice(newlineIndex + 1); if (!line) continue; handleMessage(line); } }); function handleMessage(line) { let msg; try { msg = JSON.parse(line); } catch (e) { process.stderr.write('JSON parse error: ' + e.message + '\n'); return; } if (msg.method === 'initialize') { const response = { jsonrpc: '2.0', id: msg.id, result: { protocolVersion: '0.1.0', serverInfo: { name: 'demo-server', version: '0.1.0' }, capabilities: { tools: {} } } }; process.stdout.write(JSON.stringify(response) + '\n'); } else if (msg.method === 'tools/list') { const response = { jsonrpc: '2.0', id: msg.id, result: { tools: [ { name: 'list_directory', description: '列出目录内容', inputSchema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } } ] } }; process.stdout.write(JSON.stringify(response) + '\n'); } else if (msg.id !== undefined) { const response = { jsonrpc: '2.0', id: msg.id, error: { code: -32601, message: 'Method not found' } }; process.stdout.write(JSON.stringify(response) + '\n'); } // 没有 id 的 Notification 不回复 }

这段代码做了三件事:按换行切分 stdin 数据、解析 JSON、根据method返回响应。注意process.stderr.write用于错误日志,process.stdout.write只用于 JSON-RPC 报文。Notification(没有id)直接不回复。

启动 Server:

node /absolute/path/to/mcp-server.js

然后手动发一条initialize请求。你可以另开一个终端,用管道测试:

echo '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"0.1.0","clientInfo":{"name":"test","version":"1.0.0"}}}' | node /absolute/path/to/mcp-server.js

预期输出是一行 JSON,id为 0,result.serverInfo.name为demo-server。如果你看到这行输出,说明 stdio 链路通了。

再测tools/list:

echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' | node /absolute/path/to/mcp-server.js

预期返回tools数组,里面有你声明的list_directory。最后测 Notification,发一条没有id的消息:

echo '{"jsonrpc":"2.0","method":"logging/message","params":{"level":"info","message":"test"}}' | node /absolute/path/to/mcp-server.js

预期没有任何 stdout 输出,因为 Notification 不需要响应。如果这里输出了东西,说明你的 Server 错误地给 Notification 回了响应。

跑通这三步,你就验证了 Request 的请求-响应匹配和 Notification 的静默处理。接下来把 Server 声明写进 MCP Client 的配置,Client 会自动完成initialize握手和tools/list拉取。如果 Client 里工具列表刷不出来,回到这一节用管道手动测,能快速定位是 Server 问题还是 Client 配置问题。

5. 常见报错排查:401、local proxy failed、reading choices、OAuth

这一节对照真实报错,逐个拆解。这些错误我在调试 MCP 链路时基本都踩过,按出现频率排序。

401 Unauthorized。这个错误几乎都出在模型通道的 Key 上,不是 MCP 协议层。检查三件事:Key 是否复制完整(sk-开头那串,别漏字符)、Base URL 是否写成https://taotoken.net/api(不要带多余路径或查询参数)、Key 是否已过期或被删除。如果你在 Claude Code 里看到 401,去控制台重新创建一个 Key,替换配置里的apiKey字段。注意 MCP Server 本身的 stdio 通信不涉及 401,这个错误一定来自模型 API 调用。

local proxy failed。这个报错通常出现在 Client 尝试连接模型 API 时,网络层没通。先确认你的网络能正常访问https://taotoken.net/api,可以用curl测一下:

curl -I https://taotoken.net/api

如果返回 HTTP 状态码(比如 200 或 401),说明网络通,问题在 Key 或配置。如果连接超时,检查本机网络设置。注意不要使用任何非正规的网络工具,正常网络环境下这个地址是可达的。另外确认你的 Client 没有配置额外的本地代理端口,有些工具会默认走127.0.0.1:xxxx,如果那个端口没服务就会报 local proxy failed,把代理设置关掉或指向正确地址。

reading choices 报错。这个错误一般出现在模型响应解析阶段,字面意思是读取choices字段失败。原因通常是 API 返回的不是预期格式,比如返回了一个错误对象而不是正常的 completion 结构。排查步骤:先看完整响应体,确认error字段是否存在;如果存在,按错误信息处理(多半还是 Key 或模型 ID 问题)。另一个常见原因是 Model ID 填错了,比如填了一个不存在的模型名,API 返回错误结构,Client 却按正常结构去读choices,就报这个错。确认你填的 Model ID 是 TaoToken 支持的模型标识。

OAuth 相关报错。有些 MCP Client 或工具在首次连接时会走 OAuth 流程,如果你看到 OAuth 报错,通常是因为 Client 期望的鉴权方式和你的配置不匹配。对于 TaoToken 的 API Key 模式,你不需要走 OAuth,直接在配置里填 Key 即可。如果工具强制要求 OAuth,检查是否有“使用 API Key”的选项,或者看该工具的文档是否支持自定义 Base URL + Key 的模式。Claude Code 和 Cline 都支持 API Key 直填,不需要 OAuth。

JSON 解析报错(Unexpected token)。这个错误在 MCP Server 开发中最常见,根源是 stdout 被污染。检查你的 Server 代码里有没有console.log、print、或者任何往 stdout 写非 JSON 内容的行为。所有调试信息、日志、异常堆栈都必须走 stderr。另外检查 JSON 内部有没有裸换行,JSON-RPC 报文必须单行,字符串里的换行要用\n转义。

请求发出后一直等不到响应。先确认id是否唯一且正确带回。如果 Server 返回的响应id和请求不一致,Client 匹配不上就会一直等。再确认 Server 是否真的处理了该method,如果 method 不存在且你没返回错误响应,Client 也会挂起。最后检查 stdio 缓冲:有些语言的标准输出有缓冲,需要手动 flush,否则报文卡在缓冲区里发不出来。

排查顺序建议:先用手动管道测 Server(第 4 节的方法),确认 Server 本身没问题;再检查 Client 配置里的 Base URL、Key、Model ID 三件套;最后看网络层。这样能避免在多个变量之间来回猜。

6. 把 MCP 工具链接到 TaoToken:从模型对话到 Coding Plan 的落地路径

链路跑通之后,你可以把 MCP 工具调用和 TaoToken 的模型通道组合成完整工作流。MCP Client 负责决策和工具调度,TaoToken 负责提供模型能力,两者通过 Client 的配置衔接。实际使用中,你会在 Client 里看到模型根据你的指令自动选择 MCP 工具、构造tools/call请求、拿到结果后继续推理。

如果你想先单独验证模型通道是否正常,可以用模型对话页面发一条测试消息,确认 Key 和 Base URL 没问题,地址是 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=model_chat。这一步能排除模型侧的问题,让你专注调 MCP 协议。

如果你主要做长期编码或 Agent 类任务,MCP 工具调用会非常频繁,建议了解 Coding Plan,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=coding_plan。它适合需要持续调用模型和工具的场景,能减少频繁配置的麻烦。

Key 的管理和创建在控制台,地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=console。API Key 的专门页面是 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=api_keys,创建后记得保存。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=doc,里面有各工具的配置示例。如果你用 Claude Code,专门的接入说明在 https://taotoken.net/claude-code?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=claude_code,里面有 settings 配置的完整字段。

回到 MCP 本身,最后给你一个实用技巧:在 Server 里加一个“回显”工具,把收到的params原样返回。调试 Client 时,先调这个工具,确认请求参数完整到达 Server,再调真实工具。这样能把“参数没传对”和“工具逻辑有问题”分开。另外,Notification 适合做进度推送,比如长任务执行到一半发一条logging/message,Client 收到后可以更新 UI,但不要指望它触发响应逻辑。Request 和 Notification 的边界守住,链路就稳了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询