news 2026/9/8 3:59:03

MCP协议底层机制与JSON-RPC 2.0完整生命周期解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MCP协议底层机制与JSON-RPC 2.0完整生命周期解析

先说个有意思的现象:很多人一搜“MCP 生命周期”,结果出来一半是 Vue 生命周期,一半是 Rust 的所有权生命周期,反而把真正想问的 MCP 协议本身给淹没了。MCP(Model Context Protocol)确实是现在 AI 工具链里最绕不开的一个缩写,Cursor、Claude Code、Trae、Cherry Studio 这些客户端都在接它,但天天听到“MCP Server”“MCP 工具”不代表真的理解它。我把协议规范和实际抓到的报文对照着看了几遍之后,最大的感触是:MCP 这套东西从宏观上看是“给 AI 接外部工具”,但一旦打开连接、把消息打出来看,底层流动的全是 JSON-RPC 2.0 的消息。

这篇就把 MCP 最底层的 JSON-RPC 机制、初始化协商过程、以及从建立连接到关闭的完整生命周期讲透。适合已经简单跑通过 MCP 示例、但想知道“它内部到底在干嘛”的人;如果你是零基础,也能跟着报文示例一步步建立起整体认知。

1. 先搞清楚:MCP 为什么偏要选 JSON-RPC

1.1 MCP 解决的是 AI 连接外部世界的“最后一公里”

大模型本身是没有手也没有脚的,它能回答问题、能写代码,但没法替你去查数据库、操作浏览器、提交表单。于是各家都做了自己的工具调用方案,OpenAI 有 function calling,早期 LangChain 有自己的一套工具协议,Claude 生态又有 Anthropic 自己的 tool use 格式。结果就是:每换一个模型厂商、每换一个客户端,工具代码就要重写一版,非常痛。

MCP 做的事情,就是把这个“模型 ↔ 工具”之间的通信方式做成一个公开标准。它定义了两端:MCP Host(宿主,也就是 Claude Code、Cursor 这类客户端)和 MCP Server(提供工具/资源/提示词的服务端)。Host 和 Server 之间不需要关心对方是什么语言写的、跑在哪台机器上,只需要遵守同一个消息协议就行。你可以把 MCP 类比成 USB-C 接口——以前各种外设各用各的线,现在大家统一一口,插上就用。

1.2 JSON-RPC 2.0:轻到极致的远程调用协议

JSON-RPC 2.0 这个协议本身已经有十几年历史了,是一套基于 JSON 的远程调用协议。它最大的特点就是“轻”:没有复杂的 XML 命名空间,没有一堆必须继承的基类,也不需要提前定义接口描述文件,就是用 JSON 对象表示“我要调用哪个方法、传什么参数、返回什么结果”。

MCP 选择它非常合理。一方面,MCP Server 可能是一个 Python 写的后端服务,也可能是一个 Node.js 写的本地脚本,客户端还必须支持 TypeScript、Python、Java 等多个 SDK,找一个“什么语言都能轻松解析”的消息格式是刚需。另一方面,MCP 本身的核心交互模式就是“请求-响应”,这正好和 JSON-RPC 的模型完全吻合。你在 MCP 里调一个工具,本质上就是向服务端发一条“调这个方法”的请求,服务端处理完回一条“这是结果”的响应,仅此而已。

对比一下早期的 XML-RPC 和 SOAP,你就明白为什么选它了:XML 报文冗长、解析慢,而且强类型定义那一套东西在 AI 工具调用的场景里反而成了负担。MCP 需要的是灵活、跨语言、能快速 debug 的协议,JSON 是眼下最合适的选择。

提示:JSON-RPC 2.0 规范本身只有不到一页纸的约束,MCP 则是在这个极简协议之上补充了“方法名约定”“生命周期状态”“能力协商”等规则。所以看 MCP 报文时,凡是符合 JSON-RPC 2.0 的部分都很好理解,真正要花心思的是 MCP 自己加的那些层。

1.3 一条 MCP 消息到底长什么样

JSON-RPC 2.0 的消息结构非常固定。一条请求消息长这样:

{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_weather", "arguments": { "city": "北京" } } }

字段含义:

  • jsonrpc:固定字符串"2.0",表示协议版本,防止和 1.x 版本混淆。
  • id:请求的唯一标识,可以是数字、字符串或者 null。响应里会带上相同 id,让调用方知道这是哪次请求的结果。
  • method:要调用的方法名,MCP 里都是类似initializetools/listresources/read这种带命名空间的字符串。
  • params:方法对应的参数,可选,通常是一个对象。

服务端处理完请求后返回一条响应:

{ "jsonrpc": "2.0", "id": 1, "result": { "content": [ { "type": "text", "text": "北京 晴 25°C" } ] } }

如果出错了,响应里的result会换成error

{ "jsonrpc": "2.0", "id": 1, "error": { "code": -32602, "message": "Invalid params: city is required" } }

这个结构是整个 MCP 通信的最小单元。后面无论多复杂的功能,拆到底都是这一条一条的 JSON-RPC 消息在流动。

2. 请求、响应、通知:MCP 里的三种“对话姿势”

2.1 一问一答:请求/响应是绝对主力

MCP 中绝大多数方法都走请求/响应模式。客户端发一条请求,服务端必须给一条响应,而且这个响应的id必须和请求里的id完全一致。这个规则看起来简单,但在实际并发场景下很容易出问题:如果你并发发了好几条请求,id 分别是 1、2、3,但服务端响应时把 2 的响应结果标成了 1,那么客户端就会把两条请求的结果弄混。

我用 Node.js 写过简单的调试脚本,一开始为了省事直接用的随机字符串当 id,后来排查问题的时候就发现日志里根本分不清哪条响应对应哪条请求。建议 id 在同一个连接内使用严格递增的整数,既方便日志追踪,也方便做超时控制。下面是我常用的一个伪代码流程:

let requestId = 0; function sendRequest(method, params) { const id = ++requestId; // 记录当前请求,等待响应 pendingRequests.set(id, { method, resolve, reject }); transport.send(JSON.stringify({ jsonrpc: "2.0", id, method, params })); // 设置超时 setTimeout(() => { if (pendingRequests.has(id)) { pendingRequests.delete(id); reject(new Error("Request timeout: " + method)); } }, 30000); }

这样做的好处是:任何一条响应回来,只需要查id就能知道它属于哪个请求,逻辑非常清晰。

2.2 单向广播:通知类消息不需要回复

除了请求/响应,JSON-RPC 2.0 还有一种特殊的消息叫“通知”(Notification)。通知和请求长得几乎一样,唯一的区别就是没有id字段。因为没有 id,服务端收到通知后不需要返回任何响应,也不会知道自己这条消息到底有没有被正确处理。

MCP 里最常见的一个通知是notifications/initialized,它在客户端和服务端完成初始化协商之后发出,作用是告诉服务端“我已经准备好进入正常工作了”。这类消息没有对应的响应,你如果非要等一个回执,那永远等不到。另一个常见通知是notifications/cancelled,当客户端要取消一个正在执行中的长任务时,会主动给服务端发一条取消通知。

我刚开始调试时犯过一个经典错误:在代码里手动构造了一条notifications/initialized,然后又写了等待响应的逻辑,结果直接超时。记住一个判断原则:方法名带notifications/前缀的首字母,或者干脆看 JSON 里有没有id,没有 id 就是通知,不要去等响应。

2.3 批处理:规范支持,但 MCP 明确不用

JSON-RPC 2.0 规范里还定义了一种“批处理”模式:把多个请求放在一个 JSON 数组里一次发出,例如:

[ {"jsonrpc": "2.0", "id": 1, "method": "tools/list"}, {"jsonrpc": "2.0", "id": 2, "method": "resources/list"} ]

这种模式在 RPC 领域并不罕见,能显著减少网络往返。但 MCP 的规范里明确做了约束:MCP 不使用 JSON-RPC 批处理,连接双方不能发送这种数组形式的消息。原因是 MCP 的很多方法之间有状态依赖,比如必须先初始化才能调用工具;再加上很多 MCP Server 还要支持流式响应和取消机制,把这些逻辑叠加到批处理模型上会使复杂度成倍上升,但收益并不大。所以你在用 MCP SDK 时,完全不需要实现数组形式的批处理,专心把单条消息处理好就行。

2.4 错误码:标准错误码和 MCP 的语义扩展

JSON-RPC 2.0 定义了一套固定的错误码,MCP 基本沿用了这套体系,同时预留了服务器自定义错误范围。常用错误码整理如下:

错误码名称含义
-32700Parse errorJSON 解析失败,发给服务端的不是合法 JSON
-32600Invalid Request消息结构本身不合法,比如缺jsonrpc字段
-32601Method not found方法名不存在,服务端没实现这个能力
-32602Invalid params参数不合法,比如缺少必填字段、类型错误
-32603Internal error服务端内部异常
-32000 ~ -32099Server errorMCP Server 自定义错误范围

MCP 的实现中,很多错误都是在Server error这个范围里做语义扩展的。比如请求一个不存在的资源路径时,服务端可能返回-32002,错误信息里写 “Resource not found”。这种错误码虽然不属于 JSON-RPC 标准强制约束的范畴,但实践中已经成为事实约定。调试时看到这类错误码,不要慌,直接去服务端日志里查对应实现。

注意:idnull的请求在 JSON-RPC 2.0 中表示“客户端不关心响应”,这实际上就是通知的一种写法。MCP 规范更推荐直接用“没有id字段”的消息表达通知,避免歧义。

3. 生命周期:MCP Server 从初始化到关闭的全过程

3.1 一切从 initialize 开始

MCP 的生命周期模型非常严格,大体上分为三个阶段:未初始化、已初始化、已关闭。连接刚建立的时候,双方都处于未初始化状态,此时客户端唯一能做的合法请求就是initialize

initialize请求携带的信息很关键:

{ "jsonrpc": "2.0", "id": 0, "method": "initialize", "params": { "protocolVersion": "2025-03-26", "capabilities": { "roots": { "listChanged": true }, "sampling": {} }, "clientInfo": { "name": "my-ai-client", "version": "1.0.0" } } }

看到了吗?这个请求并不是在说“帮我干某件事”,而是在说“我是什么客户端、支持哪个协议版本、我具备哪些能力”。服务端拿到之后,会根据这些信息决定是否兼容、如何协商。

有一次我犯了个低级错误:连接建立后第一件事直接发了tools/list,服务端直接秒回一个错误。后来查规范才知道,MCP 规定在完成initialize之前,客户端不能发送任何其他请求,这属于硬性约束。要让一个 MCP Server 正常工作,第一件事永远是initialize

3.2 capabilities 与 protocolVersion:双方怎么“谈条件”

服务端收到initialize后会返回一段 JSON,包括服务端支持的协议版本、服务端能力列表、服务端信息和指令。响应大概是这样的:

{ "jsonrpc": "2.0", "id": 0, "result": { "protocolVersion": "2025-03-26", "capabilities": { "tools": { "listChanged": true }, "resources": { "subscribe": true, "listChanged": true }, "prompts": { "listChanged": true }, "logging": {} }, "serverInfo": { "name": "my-mcp-server", "version": "0.1.0" }, "instructions": "这个服务提供天气查询和待办事项管理能力。" } }

这里有两个关键点要理解。

第一点:protocolVersion的协商。如果客户端发的是"2025-03-26",服务端支持"2024-11-05",那服务端会返回自己支持的最新版本"2024-11-05",客户端看到之后要么升级自己的版本,要么终止连接。网上很多教程在代码里硬编码一个协议版本字符串,其实不太规范——正确的做法是先读服务端返回的版本,再决定后续通信是否继续。

第二点:capabilities就是“我会什么”的清单。客户端看到服务端返回的capabilities里有tools字段,才会去调用tools/list;如果服务端没声明resources能力,客户端就不应该尝试resources/read。这相当于一个互相同意的能力契约,超出能力范围的调用都可能报错或者行为异常。

3.3 initialized 通知:协商完成后的“确认信”

initialize请求/响应完成之后,能不能直接开始调工具?还不行,少一步:客户端必须发送notifications/initialized通知给服务端。

{ "jsonrpc": "2.0", "method": "notifications/initialized" }

这步操作在协议层面的意义很微妙。服务端完成initialize响应后,其实已经知道了客户端的能力和版本;但客户端还必须主动告知“我知道你的能力了,现在正式开始”。这是一种双向确认机制,避免出现“服务端以为客户端知道了、客户端其实没收到响应”的错位情况。

真实世界里,很多 MCP SDK 在initialize响应返回后会自动帮你发这个通知,你在使用高级客户端时感知不到。但如果自己写底层实现,这一步非常容易漏。漏掉之后的表现很诡异:工具列表能拿、资源能枚举,但某些具体的调用会随机失败,甚至服务端日志里报“lifecycle error”。这其实是协议状态机在那提醒你:你现在还在“已经完成初始化但尚未确认”的中间状态。

3.4 运行期:工具、资源、提示词和日志

完成初始化后,MCP 就进入最活跃的阶段。这个阶段的主要操作我总结成一张表:

方法方向作用
tools/list客户端→服务端获取服务端提供的工具清单
tools/call客户端→服务端调用指定工具
resources/list客户端→服务端获取可访问的资源列表
resources/read客户端→服务端读取具体资源内容
prompts/list客户端→服务端获取提示词模板列表
prompts/get客户端→服务端获取具体提示词模板
logging/setLevel客户端→服务端设置服务端日志级别
ping任意方向保活探测,检查连接是否存活

运行期的设计有一个值得注意的点:tools/callresources/read这类方法可能会执行很长时间。比如一个工具要调外部 API,响应可能要等几十秒。MCP 对这类场景提供了进度通知机制,请求参数里的meta字段可以带上progressToken

{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "long_running_task", "arguments": {}, "_meta": { "progressToken": "task-001" } } }

服务端在执行过程中可以发送notifications/progress通知,内容是进度百分比或者具体描述。这个机制对用户体验的提升非常明显:如果客户端端只是干等,用户看到的一直是空白页面,用上进度通知就能展示实时的任务状态。

还需要注意的是运行期客户端可以随时发送ping。MCP 的传输层是长连接,如果长时间没有消息往来,任何一端都会怀疑连接已经死了。ping就是一个标准的保活手段,收到ping的一方应该回一条空的result响应。我在写本地 stdio 通信时,观察过很多 MCP Client 的实现,基本每隔几十秒就会发一次ping

3.5 关闭:优雅退出与异常断开

MCP 协议里其实没有专门设计一个“关闭”方法。生命周期到最后的收尾动作,通常就是直接关闭底层传输通道:stdio 模式下退出子进程、关掉 stdin/stdout;HTTP 模式下结束会话或直接断开连接。

但这里有一个实践上的坑:关闭之前一定要先处理正在运行中的请求。如果客户端在某个工具还没执行完时直接把进程杀了,服务端可能会产生死活锁或者资源泄漏。比较好的做法是设计一套“关闭前取消”机制:先给服务端发notifications/cancelled取消还在执行的任务,然后留出几秒的宽限期,最后再断开连接。

另外,服务端如果检测到客户端的连接异常断开(比如 EOF 或者超时),应当主动做资源清理、终止正在执行的任务。很多 MCP Server 在实现时忽略了这个环节,于是你会在服务器日志里看到大量僵尸进程挂在那里,就是因为没响应对端断开事件。

4. 传输层:stdio 与 Streamable HTTP 的取舍

4.1 stdio:最朴素的进程间通信

MCP 最早的传输方式之一就是 stdio:客户端启动一个子进程,然后把 JSON-RPC 消息写入子进程的标准输入,同时从标准输出读取子进程的响应。每一条消息用换行符分隔,一个 JSON 一行。

这种方式的优点非常明显:不需要监听端口、不需要处理 HTTP 协议、没有跨域问题,也就没有暴露网络端口的风险。本地开发调试 MCP Server 时,我强烈建议先用 stdio 模式把逻辑跑通,因为问题最好定位——直接在终端里启动 server,就能在控制台看到全部输入输出。

但 stdio 的缺点同样明显:子进程必须和客户端跑在同一台机器上,无法远程调用;而且一个 stdio 连接只能服务一个客户端,没法做多路复用。所以它更适合本地工具场景,比如“给 Claude Code 挂一个本地数据库查询工具”。

4.2 Streamable HTTP:跨机器访问 MCP 的现代方案

随着 MCP 的应用场景不再局限于本地,协议引入了 Streamable HTTP 传输方案。在这种模式下,MCP Server 变成一个 HTTP 服务,客户端通过 POST 请求发送 JSON-RPC 消息,服务端以application/json返回响应,也兼容text/event-stream的流式响应。

Streamable HTTP 解决了一个很实际的问题:MCP Server 可以部署在远程服务器上,多个客户端通过网络访问同一个 Server。比如团队内部共享一个工具服务,就不需要每个人都在本地启动一个进程了。但它也带来了新的复杂度:需要处理会话 ID(Mcp-Session-Id头)、HTTP 状态码、超时重试机制等等。

4.3 传输层和 JSON-RPC 之间的“适配层”做了什么

很多人会疑惑:JSON-RPC 消息和传输层到底是什么关系?简单说,JSON-RPC 规定了消息的“内容格式”,传输层规定了消息的“搬运方式”。MCP SDK 在中间做了一层适配:发送方把 JSON 消息序列化成字符串,交给传输层按特定格式包装;接收方把字符串解析回 JSON 对象,再分发给上层的生命周期状态机。

这个适配层的重要性在普通场景下看不出来,但一旦遇到“消息被拆包”“一行里塞了两个 JSON”“连接空闲被服务端断开”这类问题,你就会意识到:传输层除了搬运,还负责维持消息边界。stdio 模式用换行符做边界,所以要求每个 JSON 序列化后必须紧凑、不含裸换行符;HTTP 模式则天然把每次请求/响应作为一个独立消息体,边界问题由 HTTP 本身解决。

我在自测时习惯写一个小调试工具,角色类似于“中继”:

收到客户端 -> 原样解码 -> 打印 -> 原样转发给服务端 收到服务端 -> 原样解码 -> 打印 -> 原样转发给客户端

用这种方式能直观地看到消息在 stdio 上是如何被一行一行切开、又是如何在两个进程之间“来回传递”的。对理解传输层的作用非常有帮助。

5. 从零抓包:一次工具调用的完整报文流

5.1 初始化阶段:三条消息打天下

假设我们自己实现了一个极简 MCP Server,提供查询订单状态的工具。客户端连接启动后,第一条消息一定是initialize请求,服务端回复协议版本和能力,然后客户端再补一条notifications/initialized通知。整个初始化阶段其实就这三条消息:

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"debug-client","version":"0.0.1"}}} {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26","capabilities":{"tools":{"listChanged":false}},"serverInfo":{"name":"order-server","version":"1.2.0"}}} {"jsonrpc":"2.0","method":"notifications/initialized"}

注意消息里没有多余空格,这是为了在 stdio 模式下保证“一行一条”的边界清晰。很多初学者会直接用JSON.stringify(obj, null, 2)格式化后输出,结果把换行符塞进了 JSON 字符串里,导致服务端解析失败或消息提前断开。这是一个非常隐蔽的坑,调试时如果发现服务端老是报 Parse error,先检查是不是输出了带换行的格式化 JSON。

5.2 工具发现:客户端先看一眼“你有什么工具”

初始化完成后,客户端会向服务端发起tools/list,以获取当前可用工具的描述信息。下面是一次完整的工具发现请求/响应:

{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
{ "jsonrpc": "2.0", "id": 2, "result": { "tools": [ { "name": "get_order_status", "description": "根据订单号查询订单当前状态", "inputSchema": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单号" } }, "required": ["order_id"] } } ] } }

这个步骤的作用很关键:客户端一般用这个工具列表来决定如何把“用户的自然语言需求”映射到“调用哪个工具、填哪些参数”。现代模型大多能理解descriptioninputSchema的语义,所以你写工具描述时信息要足够清晰,参数约束必须准确——模型就是靠这些字段生成调用参数的。

5.3 工具调用:真正干活的时刻

工具发现之后,客户端构造tools/call请求,传入工具名和参数:

{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "get_order_status", "arguments": { "order_id": "A12345" } } }

服务端执行完逻辑后,返回一个结构相对标准化的结果:

{ "jsonrpc": "2.0", "id": 3, "result": { "content": [ { "type": "text", "text": "订单 A12345 当前状态:已发货,预计明天送达" } ], "isError": false } }

content是结果正文,可以包含多个文本块、图片块或资源链接;isError用来区分“调用成功”和“业务级失败”。注意,即使工具内部发生了业务错误(比如订单不存在),只要服务端正常处理了这次调用,传输层就还是返回result而不是error,只是isError会变成true。这个区别很多人没搞清楚,导致他们在服务端把业务异常直接抛出来变成 JSON-RPC 的error响应,结果客户端模型收到错误后体验很差,因为模型无法从错误里提取文本内容。

提示:MCP 设计isError这个字段是有讲究的。它希望工具的业务逻辑错误也能以结构化的文本结果返回给模型,让模型有机会面向用户解释这个错误,而不是把内部错误码暴露出去。你自己开发 MCP Server 时,推荐遵循这个原则。

5.4 进度通知与取消:长任务和用户的耐心

如果工具执行时间很长,客户端通常会在调用时带上progressToken,这样服务端就可以通过notifications/progress向客户端汇报进度:

{"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"task-001","progress":50,"total":100,"message":"正在查询物流信息"}}

用户如果临时决定不想要这个结果了,客户端可以发送notifications/cancelled

{"jsonrpc":"2.0","method":"notifications/cancelled","params":{"requestId":3,"reason":"用户取消"}}

这里requestId要填的是被取消请求的 id(也就是tools/call请求里的3),服务端收到后如果还在执行,应当主动中断并释放资源。我在实践中见过不少实现,收到取消通知后只是把日志打了一下,任务还在后台继续跑,这会造成严重的资源浪费,尤其是在执行 Python 数据脚本或 IA 推理类任务时。

6. 常见问题与排查实录

6.1 初始化超时:服务端半天不回 initialize 响应

现象:客户端日志报Timeout waiting for initialize response,连接建立后什么都没发生。

排查思路:先确认服务端是否真的收到了消息。stdio 模式下,如果服务端启动时输出了非 JSON 内容(例如 Python 的print调试信息、依赖库的 warning),这些内容会混入标准输出通道,客户端解析时直接出错。解决办法是把服务端所有的日志输出重定向到 stderr,绝不能让日志污染 stdout。这是实现 stdio MCP Server 的第一条纪律。

6.2 协议版本不匹配:客户端和服务端谈不拢

现象:服务端返回的protocolVersion既不是客户端发的版本,也不是客户端能接受的其他版本。常见于不同 SDK 版本实现的 Server 混用。

项目里如果同时用@modelcontextprotocol/sdk多个历史版本,很容易出现这类问题。通用解决办法是:客户端实现一个“版本回退”逻辑,收到服务端返回的版本号后,如果兼容就继续,不兼容就给用户一个明确的错误信息,而不是像某些实现那样直接静默失败。我自己遇到过一次是客户端发2025-03-26而服务端只支持2024-11-05,客户端又没有回退机制,最终所有工具调用全部失败,排查了很久才在 SDK 日志里看到版本协商异常。

6.3 请求发太早:初始化还没完成就调用工具

现象:客户端刚连接成功就立刻发tools/list,服务端返回错误码或者直接断开连接。

这是生命周期状态机最常见的违规。解决方式是在代码里维护一个显式的状态位,比如NOT_INITIALIZEDINITIALIZINGREADYCLOSED,只有状态是READY时才允许发送普通请求。像notifications/initialized通知则只能在INITIALIZING状态下发送,发完即进入READY。这种状态机逻辑虽然笨,但能挡住绝大多数协议违规问题。

6.4 并发请求 id 用重了,响应全串线

现象:两个请求的响应内容对调了,A 请求的结果跑到 B 请求头上。原因几乎都是客户端没有保证请求id唯一。

解决方式:用一个自增计数器管理id即可,不要用随机数,更不要用时间戳,高并发下重复概率比你想象的大。响应回来时最稳妥的判断是拿 id 到pendingRequests的 Map 里查找,而不是用数组遍历。

6.5 Streamable HTTP 传输下的 header 问题

现象:通过 HTTP 方式连接 MCP Server 时,能初始化成功,但调用工具后收不到响应,服务端日志里连请求记录都没有。

这类问题通常是请求没有正确携带Mcp-Session-Id头。Streamable HTTP 使用会话 ID 来区分不同的客户端会话,第一次 initialize 请求时还没有会话 ID,服务端在响应头里返回一个新的Mcp-Session-Id,之后的所有请求都必须带上它。很多 SDK 把初始化响应的 header 解析逻辑藏得很深,如果你自己封装 HTTP 调用,一定要在initialize响应之后把Mcp-Session-Id存下来,后续每个请求都加到 header 里。

7. 写在最后:一点实操经验

协议这东西,光看规范永远记不牢,一定要自己动手抓报文。我最推荐的方式是:随便选一个 MCP SDK,本地跑一个最简单的 echo server,然后用官方调试工具去连它,把所有收发消息打印到终端。看几条真实的initializetools/listtools/call报文之后,你对 MCP 的理解会立刻超过一大半只看过视频教程的人。

另外一个建议是,不要在项目里盲目引入一堆高级封装,先试着用原生 SDK 实现一次完整的工具调用链路。你会在过程中踩到协议版本、超时、生命周期状态、stdout 污染、会话 ID 这些坑,而这些坑恰恰是 MCP 协议设计里最值钱的经验。把一次调用完整跑通,再回头用那些封装好的框架,你会觉得它们做的事情清晰得多了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 3:58:05

数控铣削开粗加工全流程指南:刀具参数与刀路策略精讲

做机加工和数控编程这些年,我最大的体会是:开粗这个环节虽然听起来粗犷,但恰恰是整个零件加工中最容易出问题、也最考验工艺功底的部分。零件能不能按期交、刀具寿命高不高、精加工是否稳定,往往在开粗阶段就已经决定了。很多初学…

作者头像 李华
网站建设 2026/9/8 3:56:59

微信小程序纯前端GIF生成:canvas帧采集与编码器实践

简介:面向微信小程序开发者与前端爱好者的 GIF 动画制作项目,依托小程序端实现动图编辑、预览与一键分享,解决了移动端轻量化制作 GIF 动画的需求,无需依赖笨重的桌面软件,门槛较低。压缩包共 69 个文件、约 1.85MB&am…

作者头像 李华
网站建设 2026/9/8 3:56:51

办公AI助手实测对比:豆包、Kimi、文心一言、通义千问怎么选

1. 先别急着装,搞明白“办公AI助手”到底在解决什么问题 过去一年多,办公AI助手这个赛道杀成了一片红海。今天你装个豆包,明天同事推Kimi,后天老板说要统一用文心一言,再过一阵子通义千问又出了个新功能。工具越装越多…

作者头像 李华
网站建设 2026/9/8 3:56:02

零售业人流仿真:SimWalk建模实战与动线优化指南

1. 零售业为什么要做人流仿真:从直觉判断到量化决策 先聊一个让我印象很深的案例。去年帮一家连锁商超做改造前的动线评估,运营主管给我的原始诉求只有一句话:“感觉周末收银区太挤了,想看看能不能多开两个收银台。”但当我们把 …

作者头像 李华
网站建设 2026/9/8 3:53:26

基于STM32的智能医疗输液监控系统设计与仿真全开源

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华