
1. 为什么你的 MCP 工具总是握手失败从 JSON-RPC 2.0 消息格式说起MCP 协议Model Context Protocol是让 AI 客户端调用外部工具的一套通信规范它规定了消息长什么样、怎么传、什么时候建立连接。适合谁适合正在给 Claude Desktop、Cline、Cursor 这类客户端写工具服务端或者想把本地脚本暴露成 AI 可调用能力的开发者。很多人第一次写 MCP Server代码跑起来了客户端却报initialize超时或者Method not found根因往往不在业务逻辑而在协议层——JSON-RPC 2.0 的消息格式没对齐或者传输层选错了。我先把 MCP 的协议栈拆成三层来看这样后面排错有坐标应用层是 Host宿主比如 IDE 插件、Claude Desktop和 Server你写的工具服务端。协议层是 JSON-RPC 2.0 消息格式加上 MCP 定义的原语Tools / Resources / Prompts。传输层是 stdio本地子进程管道或 SSE远程 HTTP 流式。为什么 MCP 选 JSON-RPC 2.0 而不是 gRPC 或 REST三个原因很实在规范只有一页纸实现成本极低传输无关同一套消息能跑在 stdio、HTTP、WebSocket 上原生支持通知Notification这种不需要响应的异步消息适合流式场景。JSON-RPC 2.0 只有三种消息类型记住它们90% 的格式错误都能定位。请求Request必须带jsonrpc、id、methodparams可选{ jsonrpc: 2.0, id: 1, method: tools/call, params: { name: get_weather, arguments: { city: Beijing } } }成功响应带回同一个id和result{ jsonrpc: 2.0, id: 1, result: { content: [ { type: text, text: 北京当前温度28°C } ] } }错误响应把result换成error里面是code、message、可选data{ jsonrpc: 2.0, id: 1, error: { code: -32603, message: Internal error, data: { details: API rate limit exceeded } } }通知Notification没有id发出后不需要响应比如握手完成后的确认{ jsonrpc: 2.0, method: notifications/initialized, params: {} }MCP 在标准错误码之上做了扩展排错时对照这张表能省很多时间错误码含义典型触发场景-32700Parse errorJSON 本身不合法多逗号、缺引号-32600Invalid Request缺jsonrpc或method字段-32601Method not found方法名拼错或服务端没注册该能力-32602Invalid params参数 schema 不匹配-32603Internal error服务端内部异常-32000 ~ -32099Server error自定义服务端错误-32100Resource not foundMCP 扩展资源 URI 不存在-32101Tool execution errorMCP 扩展工具执行失败MCP 的三大原语决定了功能语义Tools 负责“做什么”通过tools/call调用Resources 负责“读什么”通过resources/read读取Prompts 负责“怎么说”通过prompts/get获取模板。一句话概括就是 Tools 写、Resources 读、Prompts 说。这里有个容易踩的坑id的类型必须前后一致。客户端发id: 1数字服务端回id: 1字符串严格校验的客户端会认为响应无法匹配直接丢弃。我见过有人排查半天最后发现是序列化库把数字 id 转成了字符串。2. TaoToken 统一接入一个 Key 打通多模型与 MCP 工具链写 MCP Server 时工具内部往往要调用大模型——比如一个“代码解释”工具背后得请求模型。如果每个工具都单独配一套模型 Key、单独处理不同厂商的 Base URL 和鉴权格式配置会迅速失控。TaoToken 在这里的作用是提供统一的 API 通道一个 Key、一个 Base URL兼容主流模型调用格式工具侧只需要改配置不用改业务代码。TaoToken 是什么、能做什么它是一个模型 API 聚合接入服务对外暴露统一的 OpenAI 兼容接口。你可以把它理解成“模型调用的统一插座”——不管底层接的是哪家模型你的 MCP 工具只认一个地址和一个 Key。适合谁适合手上有多个 MCP 工具、每个工具都要调模型、又不想维护多套鉴权逻辑的开发者。接入前你需要准备三样东西我称之为“三件套”缺一不可Base URLhttps://taotoken.net/api注意 API 地址不带任何查询参数API Key在控制台创建形如sk-开头的一串字符Model ID你要调用的具体模型标识比如claude-sonnet-4-5这类这三件套在 MCP 工具里的落点很明确Base URL 决定请求发往哪里API Key 决定鉴权是否通过Model ID 决定实际调用哪个模型。任何一处写错表现出的报错都不一样后面第五节会逐一对照。先拿 Key。打开控制台页面创建 API Keyhttps://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_jsonrpc_transport创建后立刻复制保存页面刷新后完整 Key 不再显示。如果你只是想先验证模型通道是否通可以用模型对话页面直接试https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_jsonrpc_transport长期做编码类 MCP 工具、需要稳定额度的看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_jsonrpc_transportKey 管理入口在 API Keys 页面https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_jsonrpc_transport接入文档在这里参数细节以文档为准https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_jsonrpc_transport为什么要在 MCP 场景里强调统一接入因为 MCP 工具的执行链路是“客户端 → MCP Server → 模型 API”。这条链路上MCP Server 是中间层它既要处理 JSON-RPC 消息又要发起模型请求。如果模型请求这层用了五花八门的 SDK 和鉴权方式一旦某个厂商改了接口你的 MCP Server 就得跟着改。统一到 OpenAI 兼容格式后模型侧的变化被隔离在配置层协议层和业务层不受影响。一个实际的配置思路把 Base URL、API Key、Model ID 抽成环境变量MCP Server 启动时读取。这样本地调试和线上部署用同一份代码只换环境变量。下一节给出可直接复制的配置片段。3. 可复制配置stdio 与 SSE 双传输层 三件套落地这一节给可直接复制的配置。先讲传输层选择再给三件套的配置片段。stdio 传输通过子进程的 stdin/stdout 传 JSON-RPC 消息零网络开销延迟最低安全性高——子进程在本地跑没有网络暴露面。适合 CLI 工具、本地集成、开发调试。SSE 传输是服务器通过 SSE 向客户端推送、客户端通过 HTTP POST 向服务器发送的混合模式适合远程 API、微服务、多客户端共享。先看 stdio 服务端的最小实现重点是消息读写走标准流import asyncio import json import sys async def read_message(): 从 stdin 读取一行 JSON-RPC 消息 line await asyncio.get_event_loop().run_in_executor(None, sys.stdin.readline) if not line: return None return json.loads(line) async def write_message(msg: dict): 向 stdout 写入一行 JSON-RPC 消息 sys.stdout.write(json.dumps(msg) \n) sys.stdout.flush() async def main(): while True: msg await read_message() if msg is None: break # 处理 initialize 握手 if msg.get(method) initialize: await write_message({ jsonrpc: 2.0, id: msg[id], result: { protocolVersion: 2024-11-05, capabilities: {tools: {}}, serverInfo: {name: demo-server, version: 1.0.0} } }) elif msg.get(method) tools/list: await write_message({ jsonrpc: 2.0, id: msg[id], result: {tools: []} }) if __name__ __main__: asyncio.run(main())客户端侧连接 stdio 服务端配置片段如下以通用 JSON 配置为例路径按你的实际项目改{ mcpServers: { demo-stdio: { command: python, args: [/path/to/your/server.py], env: { TAOTOKEN_BASE_URL: https://taotoken.net/api, TAOTOKEN_API_KEY: sk-你的Key, TAOTOKEN_MODEL_ID: claude-sonnet-4-5 } } } }SSE 服务端的配置片段关键是两个端点/sse负责服务器到客户端的流式推送/messages负责客户端到服务器的 POST{ mcpServers: { demo-sse: { url: http://localhost:8000/sse, env: { TAOTOKEN_BASE_URL: https://taotoken.net/api, TAOTOKEN_API_KEY: sk-你的Key, TAOTOKEN_MODEL_ID: claude-sonnet-4-5 } } } }如果你用的是 Claude Code 这类工具配置走 settings 文件三件套同样落在这三个字段上{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的Key, ANTHROPIC_MODEL: claude-sonnet-4-5 } }注意这里 Base URL 写的是https://taotoken.net/api不带任何查询参数。有人习惯性把带 UTM 的官网地址填进去结果请求 404——官网地址是给人看的API 地址是给程序调的两者不能混。Codex 的auth.json配置也是同样的三件套逻辑{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model: claude-sonnet-4-5 }Cline 的 MCP 配置里如果工具需要调模型同样把三件套塞进环境变量。CC Switch 切换配置时本质就是切换这三件套的值。一个实用技巧把三件套写进.env文件配置里用变量引用避免 Key 硬编码进版本库。.env加进.gitignore团队协作时每人填自己的 Key。4. 用 curl 验证初始化握手与工具列表返回配置写完别急着接客户端先用 curl 手动跑一遍 JSON-RPC 消息确认服务端行为符合预期。这一步能提前暴露 90% 的协议层问题。先验证 SSE 服务端的初始化握手。SSE 是长连接curl 要加-N禁用缓冲才能实时看到推送curl -N http://localhost:8000/sse正常会看到类似这样的流式输出第一行是 event 类型第二行是数据event: endpoint data: /messages?sessionIdabc123拿到sessionId后向/messages端点 POST 初始化请求curl -X POST http://localhost:8000/messages?sessionIdabc123 \ -H Content-Type: application/json \ -d { jsonrpc: 2.0, id: 0, method: initialize, params: { protocolVersion: 2024-11-05, capabilities: { tools: {} }, clientInfo: { name: curl-test, version: 1.0.0 } } }成功的响应会通过 SSE 通道推回来内容形如{ jsonrpc: 2.0, id: 0, result: { protocolVersion: 2024-11-05, capabilities: { tools: {}, prompts: {} }, serverInfo: { name: demo-server, version: 1.0.0 } } }握手成功后发notifications/initialized确认注意没有idcurl -X POST http://localhost:8000/messages?sessionIdabc123 \ -H Content-Type: application/json \ -d { jsonrpc: 2.0, method: notifications/initialized, params: {} }然后请求工具列表curl -X POST http://localhost:8000/messages?sessionIdabc123 \ -H Content-Type: application/json \ -d { jsonrpc: 2.0, id: 1, method: tools/list, params: {} }返回的result.tools数组里就是服务端注册的所有工具每个工具带name、description、inputSchema。如果这里是空数组说明服务端没注册工具或者注册逻辑没被执行。验证 stdio 服务端更简单直接把 JSON-RPC 消息喂给进程的 stdinecho {jsonrpc:2.0,id:0,method:initialize,params:{protocolVersion:2024-11-05,capabilities:{},clientInfo:{name:curl-test,version:1.0.0}}} | python /path/to/your/server.py正常会看到一行 JSON 响应打印到 stdout。如果没有任何输出检查服务端是不是在等更多输入或者 stdout 被日志污染了——stdio 传输下stdout 只能放 JSON-RPC 消息日志必须走 stderr否则客户端解析会失败。这是 stdio 场景最高频的坑之一。验证模型通道是否通可以直接 curl TaoToken 的接口curl https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer sk-你的Key \ -d { model: claude-sonnet-4-5, messages: [{role: user, content: ping}] }返回带choices数组就说明三件套配置正确。这一步单独验证能把“模型通道问题”和“MCP 协议问题”隔离开。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth这一节对照真实报错逐个拆解。每个报错都对应三件套或协议层的某个具体问题。401 Unauthorized。这是鉴权失败几乎都是 API Key 的问题。三种可能Key 复制时带了空格或换行Key 已过期或被删除请求头格式不对。检查Authorization头是不是Bearer sk-xxx格式Bearer和 Key 之间一个空格。如果用的是 Claude Code 的 settings 配置确认字段名是ANTHROPIC_API_KEY而不是别的。重新去 API Keys 页面生成一个 Key 再试https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_jsonrpc_transportlocal proxy failed。这个报错通常出现在客户端尝试连接 MCP Server 时本地代理或子进程启动失败。排查顺序先确认command字段指向的可执行文件路径正确python是不是在 PATH 里再确认args里的脚本路径是绝对路径相对路径在不同工作目录下会失效最后看子进程有没有立刻退出——手动在终端跑一遍同样的命令看报什么错。如果是 SSE 场景报这个检查端口是不是被占用url字段的地址和端口是否和服务端实际监听一致。reading choices 相关报错。这类报错说明模型 API 返回的响应结构不符合预期客户端在解析choices字段时失败。根因通常是 Base URL 写错了——比如把官网地址https://taotoken.net当成了 API 地址请求打到了网页而不是接口返回的是 HTML自然没有choices。正确地址是https://taotoken.net/api。另一个可能是 Model ID 写错请求了一个不存在的模型服务端返回错误结构。用第四节的 curl 命令单独验证模型通道能快速定位。OAuth 相关报错。如果客户端提示 OAuth 认证失败或 token 无效说明它走的是 OAuth 流程而不是 API Key 流程。MCP 生态里有些客户端默认走 OAuth你需要在其配置里显式指定用 API Key 鉴权。检查配置里有没有auth或oauth相关字段把它改成 API Key 模式。Claude Code 场景下确认ANTHROPIC_API_KEY已设置且没有残留的 OAuth token 缓存——清掉旧的凭据缓存再重启。Method not found-32601。方法名拼错或者服务端没注册对应能力。MCP 的方法名是固定的initialize、tools/list、tools/call、resources/read、prompts/get。检查大小写和斜杠。如果服务端声明了capabilities里没有tools客户端调tools/list也会失败。id 不匹配导致响应被丢弃。前面提过id类型必须前后一致。数字1和字符串1在严格校验下不相等。检查你的 JSON 序列化逻辑确保原样回传客户端的id。stdio 下 stdout 被日志污染。表现是客户端报 JSON 解析错误-32700。根因是服务端把日志打到了 stdout。修复方法所有日志走sys.stderrstdout 只输出 JSON-RPC 消息。Python 里用print(..., filesys.stderr)或者配置 logging 输出到 stderr。SSE 连接建立后收不到消息。检查 curl 有没有加-N检查服务端有没有正确 flush检查中间有没有反向代理做了缓冲。SSE 对缓冲很敏感Nginx 场景下需要关闭proxy_buffering。排错时记住一个原则先隔离层次。模型通道问题用 curl 单独验证MCP 协议问题用 curl 手动发 JSON-RPC 验证客户端问题看客户端日志。三层分开测比在一个黑盒里猜快得多。接入文档里有更详细的参数说明和错误码对照https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_jsonrpc_transport6. 把 MCP 工具链跑通之后统一通道与协议层的分工MCP 协议的设计哲学是最小约定、最大自由。它不假设你的工具做什么不限制你的传输方式只定义消息长什么样、怎么传、何时建立连接。JSON-RPC 2.0 负责消息格式stdio 和 SSE 负责传输握手阶段负责能力协商。理解这三层排错就有坐标。TaoToken 在这条链路里的位置是模型调用的统一出口。MCP Server 处理协议层TaoToken 处理模型通道两者职责清晰。三件套Base URL、API Key、Model ID是连接这两层的接口配置对了模型侧的变化就不会传导到协议层。如果你还在选模型通道先用模型对话页面试一下手感https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_jsonrpc_transport长期跑编码类 MCP 工具、需要稳定额度的Coding Plan 更合适https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmcp_jsonrpc_transport最后留一个我踩过的坑stdio 服务端调试时别用print打日志一定走 stderr。这个坑我花了半小时才定位到因为客户端只报 JSON 解析错误不告诉你哪来的脏数据。把日志和协议消息分开是写 MCP Server 的第一条纪律。