跳到主要内容

连接不是一根干净的管子 — 报进度、报日志、清单会长、线会断

这一章讲三件事: 一次跑很久的调用中间会发生什么、你能看到什么; 线断了怎么接上;以及清单太长或者中途变了怎么办。

它在全书链条里的位置: 第 05、06 章讲的都是「一问一答、马上有结果」。 这一章处理那条连接上除了一问一答之外的所有东西它也是全书里书稿最不完整的一章——两个小节在原书里各只有三四行(见第 7 节)。

1. 四件事,四个答案

这一节先给全景:一件跑得久的活会带出哪几个问题。

回想第 05 章那次调用:发一条、等一下、收一条,一秒之内结束。 换成一次要跑三分钟的调用,四个问题立刻冒出来:

问题用户会问什么协议给的办法本章第几节
它现在到哪一步了?「卡住了吗?」服务器一路报进度§3
它中间出过什么状况?「为什么少了一个?」服务器把自己的日志发过来§4
清单太长怎么办?(用户看不见,但工具会莫名其妙消失)一次给一页,附一个接着取的记号§6
线断了怎么办?「白等三分钟?」报出最后收到的那条,从那儿往后补§5

前两个和最后一个共用同一样东西,先讲那样东西。

2. 通知:不需要回话的那一类消息

这一节是本章主走查的第 1 步。

本章主走查的输入:用户说「把项目里的说明文档全转成 PDF」, 一共 214 个文件,这次调用跑了 3 分钟。 214、3 分钟以及后面出现的所有具体数值,都是我们为演示编的,不是真实数值。 书里这几节没有给场景,只有方法名。

回看第 04 章那三种报文:请求带编号、回复带同一个编号、还有一种不带编号的。 第三种就是通知——发出去就完事、不等回复的那类消息

书里说得很简练:协议允许服务器(客户端也一样)发送通知消息, 之后怎么处理这些通知,「由接收方自己决定」1

最后半句很重要:「由接收方自己决定」意味着你可以完全不理它。 不理它不会报错,也不会卡住——你只是什么都看不到。

主走查的第 1 步是:客户端发出那条「转 214 个文件」的调用请求, 并且在请求里附上一个标识,表示「这一件活的进度请报给我」。 然后这条请求会挂在那儿三分钟。接下来的一切,都靠通知送回来。

3. 进度:每一步报一个数

这一节是主走查的第 2 步。

进度不是应用自己估着画的。它是服务器一条一条报回来的,每条里有三样:

里面有什么说明
一个标识(progressToken)conv-7客户端在发请求时定的,用来认领这是哪一件活的进度
已完成量123214必须一次比一次大
总量(可选)214有它才画得出百分比;没有就只能显示「已完成 137」
一句话(可选)「正在转 chapter-03.md」给人看的
客户端发请求 → { … , "_meta": { "progressToken": "conv-7" } }
↑ 这个标识就是「请报进度」的开关,不写就没有进度
服务器一路报 ← { "method": "notifications/progress",
"params": { "progressToken": "conv-7",
"progress": 137, "total": 214 } }
三分钟后回结果 ← { "id": …, "result": { … } }

图说:214 个文件、3 分钟,平均每秒转一个多一点。
如果没有这条进度线,用户面对的就是三分钟的空白。

这个数到底代表什么,由服务器说了算。 它可以按文件数报,也可以按已经处理了多少数据量来报, 甚至按它自己觉得合适的任何单位报——客户端只能照显。 书里在讲客户端那个发进度的方法时,也把这一点说得很直白: 这些值最终长什么样,取决于你连的是哪台服务器、它对进度更新的要求是什么2

这里有一处必须点明的方向差异。 书里介绍的那个方法(send_progress_notification()) 是客户端往服务器报进度;而上面这条走查用的是服务器往客户端报。 两个方向协议里都有,但书里只写了前者,后者是我们照官方规范补的3。 你在写客户端时真正需要的,几乎总是后者。

4. 日志:让服务器把话说到你这边来

这一节是主走查的第 3 步。

日志程序在运行时留下的一行行记录:什么时候干了什么、出了什么岔子。 默认它们只留在服务器那台机器上——而 MCP 让服务器可以把它们发到你这边来。

书里的说法是:服务器发起的日志通过通知送到客户端; 而且它和别的通知不一样——它在会话构造函数里有自己专门的一个参数口4。 你写一个函数挂上去(就是第 07 章那个套路),日志就会流进这个函数。

我们的走查在这里发生了一件事:

转到第 137 个文件时,服务器发来一条:
level = "warning"
logger = "md2pdf"
data = { "file": "api-reference.md", "reason": "表格结构损坏,已跳过" }

图说:214 个文件里只有这 1 条警告,其余 213 个正常。
没有这条日志,用户只会看到最后少了一个 PDF,却不知道为什么。

那个 level日志级别——这条记录有多严重它不是谁临时定的,MCP 用的是现成的一套标准:RFC 5424 里的八级严重度5, 从轻到重是:

级别大意什么时候用
debug / info / notice调试细节 / 一般信息 / 值得记一笔的正常事件平时
warning有问题但还能继续我们那条就是这一级
error / critical这一步失败了 / 某个部件挂了要处理
alert / emergency必须立刻有人管 / 整个系统不能用了极少

为什么用现成的标准而不是自己定? 因为你的日志系统、你的告警规则、 你运维同事的经验,全都是按这八级来的——换一套等于让所有下游重学一遍。

书里那个示例的处理函数,只把 error 及以上的四级打出来,其余的丢掉6这是个合理的默认:如果你把 debug 也打到用户界面上,界面会被淹掉。

5. 断线:书里写的那套续传办法

这一节是主走查的第 4 步,也是这一章唯一在最新规范里被整个删掉的机制。

场景:转到第 180 个文件的时候,网断了。

普通的做法是整件事重来——三分钟白等,而且前 179 个文件白转了。 书里给的办法是从断点接上。 这件事叫断线续传:线断了不用整个重来, 从最后收到的那一条之后接着补。

它的做法是:服务器往那条长连接上吐的每一条消息都带一个编号; 客户端记住自己最后收到的是哪一个;重连的时候把这个编号报上去, 服务器就从那一条之后接着补发7

断之前: … 消息 #178 → #179 → #180 ✔ 收到 → ✖ 断了
重连时: 客户端说:「我最后收到的是 #180。」
服务器: 从 #181 开始补发 → #181 #182 … 一直到结束

图说:关键在「服务器要一直记着自己发过哪些消息」——
没有这份记忆,它不知道 #181 是什么。这句话是第 12 章的伏笔。

规范对这件事的措辞很克制: 客户端「应当」这么做,服务器「可以」这么补—— 也就是说,服务器完全可以不支持,那你就只能整个重来7

还有一条硬规定:服务器「禁止」把本该发在另一条连接上的消息补到这一条上7理由是:两条连接可能属于两件互不相干的活,混了就乱了。

记住上面那句伏笔:这套办法要求服务器一直记着自己发过什么。 第 12 章会告诉你,正是这个要求让它被删掉了。

6. 另起一处走查:清单太长,而且它还会变

这两件事都发生在连接期间,但都不在上面那条三分钟的走查上,所以单独走一遍。

下面这条走查的输入:客户端刚连上一台文档服务器,第一件事是要工具清单。 60、180 这两个数是我们为演示编的。

第一件:清单会分页**——一次只给你一部分,想要后面的还得再问一次

① 客户端:tools/list(不带记号)
服务器:60 个工具 + nextCursor = "eyJwYWdlIjoyfQ=="
↑ 这个记号叫**游标**
② 客户端:tools/list(带上那个记号)
服务器:又 60 个 + 新的记号
③ 再取一次:最后 60 个,**没有记号了** → 到头了

图说:180 个工具分了三页。客户端要是只取第一页就收工,
后面 120 个工具就等于不存在——**而且不会报任何错。**

游标就是服务器给你的一个「接着从这儿取」的记号。 书里对它的说明只有一句半:分页结果里有一个「下一个游标」属性, 而所有列清单的方法都收一个游标参数8

两条使用规矩要记住:

  • 记号是不透明的:它可能长得像一段编码,但你不许去拆它、不许猜它的含义——只管原样传回去;
  • 拿不到记号才算到头;拿到了就必须接着取,否则就是悄悄漏掉了后面的东西

第二件:清单会在你连着的时候变。

服务器可以在工具清单、话术清单、资源清单变了的时候给你打个招呼, 也可以在某一份资源本身改了的时候通知你——前提是你先做过一次订阅: 向服务器登记一句「这东西变了就告诉我」,登记完它就记着你,变了才会来说。

书里给了两条应对,并且明确说是二选一9:

做法代价
每轮重取:用户每输入一次,就重新要一遍清单每轮多一次来回;工具多的时候还多一大堆流量
听通知:挂一个函数等服务器来说省流量,但当时的 Python 工具包做不到(见下一节)

书里示例代码走的是第三条路——在程序启动时取一次就不再管了, 并且作者自己在正文里点明:如果你预计服务器会在连着的时候改工具, 你就得改成上面两条中的一条9

7. 边界:这一块书自己说工具包还没做好

这一节说清楚:本章讲的东西,当时有多少是真能用的。

书里在两个地方留了同一句坦白:

协议里有这些通知,但当时的 Python 工具包「没有面向用户的接口」 来处理和监听它们;想用就得自己把消息路由到回调上, 作者建议照着工具包源码里采样回调的做法抄,或者写一个「处理所有消息」的总函数 从会话的另一个参数口塞进去10

这就是为什么本章第 6 节那张表里,「听通知」这条路当时是走不通的。

另外两处不完整:

小节原书里的状态
断线续传整节只有四行,而且开头一行是作者写给自己的提纲:「从协议规范抄过来,然后讲 Python 工具包怎么做」——后半句没有写11
分页整节只有三行,同样是提纲式的:「哪些结果会分页、游标怎么拿、怎么用」8

还有两件事,你现在就该知道它们后来变了:

  1. 日志这条路后来被列进了移除队列,替代方案是让服务器写到标准错误流、 或者接一套通用的观测工具(第 12 章那张表里有它); 规范只给了这条替代路子,没有说为什么要弃用它——这一点后面也补不出来,照实写在这儿;
  2. 订阅的形状被整个换掉了:那条「服务器主动往长连接上推」的路没有了, 换成客户端主动登记一条长回话(第 12 章)。

8. 可带走的

  1. 一件跑得久的活带出四个问题:报进度、报日志、清单太长、线会断;
  2. 通知 = 不带编号、不必回话的消息;进度和日志都靠它送,收不收得住由接收方自己决定;
  3. 进度靠一个标识认领:客户端发请求时附上它,不附就没有进度;
  4. 进度的单位由服务器说了算,客户端只能照显;已完成量必须一次比一次大;
  5. 书里写的进度方法是「客户端报给服务器」,你真正需要的那个方向要看规范;
  6. 日志级别用的是现成的八级标准(RFC 5424),不是谁临时定的——换一套等于让所有下游重学;
  7. 清单可能是分页的:拿到游标就必须接着取,不取就是悄悄漏掉后面的工具,而且不报错;
  8. 游标不透明:不许拆开、不许猜,原样传回去;
  9. 断线续传靠「我最后收到的是第几条」,而这要求服务器一直记着自己发过什么——记住这句,第 12 章要用;
  10. 书里明写这一块当时的 Python 工具包没做好,断线与分页两节各只有三四行。

9. 原文地图

主题原书章原文位置
通知是什么、由接收方处理Interacting with MCP Server Capabilitiestext/08-fm-interacting-with-mcp-server-capabilities.txt:19(搜「allows notification messages to be sent by the」) · :20(搜「up to the receiver to」)
客户端发进度的那个方法Interacting with MCP Server Capabilitiestext/08-fm-interacting-with-mcp-server-capabilities.txt:871(搜「sends a notification to the server with」) · :872(搜「the amount of progress as a float」)
日志走通知、有专门的参数口Interacting with MCP Server Capabilitiestext/08-fm-interacting-with-mcp-server-capabilities.txt:834(搜「Server-initiated logs are sent to the client via notifications」) · :836(搜「logging_callback」)
八级严重度标准Interacting with MCP Server Capabilitiestext/08-fm-interacting-with-mcp-server-capabilities.txt:866(搜「syslog severity levels defined in」)
示例只打 error 以上Interacting with MCP Server Capabilitiestext/08-fm-interacting-with-mcp-server-capabilities.txt:863(搜「checks the reported」)
断线续传那套办法Resuming Connectionstext/12-fm-resuming-connections.txt:7(搜「issue an HTTP GET to the MCP endpoint」) · :11(搜「replay messages that would have」) · :15(搜「MUST NOT replay messages」)
分页:游标与列表参数Paginating Resultstext/13-fm-paginating-results.txt:6(搜「has a nextCursor property」) · text/08-fm-interacting-with-mcp-server-capabilities.txt:31(搜「cursor parameter for pagination」)
清单会变:两条应对Interacting with MCP Server Capabilitiestext/08-fm-interacting-with-mcp-server-capabilities.txt:274(搜「get a fresh list of available tools」) · :276(搜「handle a list_changed notification from the server」)
SDK 没有处理通知的口子Interacting with MCP Server Capabilitiestext/08-fm-interacting-with-mcp-server-capabilities.txt:671(搜「no user-facing API for handling and」) · :422(搜「partially implemented in the Python SDK」)

Footnotes

  1. 出处:「Interacting with MCP Server Capabilities」第 19 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:19,搜「allows notification messages to be sent by the」)与第 20 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:20,搜「up to the receiver to」)。原文还补了一句实现细节:走远程线路时这类消息和别的消息一样发,只是类型是 JSONRPCNotification

  2. 出处:「Interacting with MCP Server Capabilities」第 871 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:871,搜「sends a notification to the server with」)与第 872 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:872,搜「the amount of progress as a float」)。原文列的是:一个用来认领的标识、一个小数表示的已完成量,可选的总量和一句消息。

  3. 补充(不在书里):官方规范里的进度是这样定义的——客户端在请求的 _meta 里放一个 progressToken(必须是字符串或整数,且在所有进行中的请求里唯一),服务器可以据此发 notifications/progress,里面带 progressTokenprogress,以及可选的 totalmessage;progress 必须每次递增,哪怕总量未知。服务器也可以选择一条都不发。来源:MCP 官方规范进度页 https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/progress(查阅于 2026-08-25)。

  4. 出处:「Interacting with MCP Server Capabilities」第 834 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:834,搜「Server-initiated logs are sent to the client via notifications」)与第 836 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:836,搜「logging_callback」)。同一段还提到另一个方法可以在服务器允许的情况下改这次连接的日志级别(第 831 段,text/08-fm-interacting-with-mcp-server-capabilities.txt:831,搜「set_logging_level」)——这个方法在 2026-07-28 版规范里已被移除,级别改成每条请求自己带。

  5. 出处:「Interacting with MCP Server Capabilities」第 866 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:866,搜「syslog severity levels defined in」)。补充(不在书里):八级的完整名单与用途见官方规范日志页——debug、info、notice、warning、error、critical、alert、emergency。来源:https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/logging(查阅于 2026-08-25)。

  6. 出处:「Interacting with MCP Server Capabilities」第 863 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:863,搜「checks the reported」)。原文那个函数只处理 error、critical、alert、emergency 四级,并说明这份清单「并不完整」。除了级别和内容,这类消息还可以带一个可选的记录器名字(第 868 段,text/08-fm-interacting-with-mcp-server-capabilities.txt:869,搜「optional logger name」)。

  7. 出处:「Resuming Connections」第 7 段(text/12-fm-resuming-connections.txt:7,搜「issue an HTTP GET to the MCP endpoint」)、第 11 段(text/12-fm-resuming-connections.txt:11,搜「replay messages that would have」)与第 15 段(text/12-fm-resuming-connections.txt:15,搜「MUST NOT replay messages」)。这一整节在原书里只有四行,而且前面还有一行是作者写给自己的提纲(第 3 段,text/12-fm-resuming-connections.txt:3,搜「from the protocol spec」)。 2 3

  8. 出处:「Paginating Results」第 6 段(text/13-fm-paginating-results.txt:6,搜「has a nextCursor property」)。这一整节在原书里只有三行,而且是提纲式的,连一个完整句子都没有。客户端该怎么用见「Interacting with MCP Server Capabilities」第 31 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:31,搜「cursor parameter for pagination」)。补充(不在书里):规范规定游标是不透明的,客户端禁止解析或修改它;支持分页的是四个列清单方法。来源:https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/pagination(查阅于 2026-08-25)。 2

  9. 出处:「Interacting with MCP Server Capabilities」第 274 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:274,搜「get a fresh list of available tools」)与第 276 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:276,搜「handle a list_changed notification from the server」)。原文的两条路是:每次用户输入都重取一份清单,或者让客户端监听并处理服务器发来的「清单变了」通知。资源那一侧的订阅方法见第 336 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:336,搜「resources/subscribe or subscribe_resource()」)。 2

  10. 出处:「Interacting with MCP Server Capabilities」第 671 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:671,搜「no user-facing API for handling and」)。同样的话在资源那一节也说过一遍(第 422 段,text/08-fm-interacting-with-mcp-server-capabilities.txt:422,搜「partially implemented in the Python SDK」),并说 TypeScript 那一版似乎有。作者给的变通办法之一是写一个总处理函数,从会话构造函数的另一个参数口塞进去(第 679 段,text/08-fm-interacting-with-mcp-server-capabilities.txt:679,搜「message_handler」)。

  11. 出处:「Resuming Connections」第 3 段(text/12-fm-resuming-connections.txt:3,搜「from the protocol spec」)。原文这一行是作者给自己写的提纲:「续传(远程客户端)—— 先从协议规范抄,然后讲 Python 工具包怎么做」。后半句在这一版里没有兑现。