跳到主要内容

接上一台服务器 — 两种线路和一次打招呼

这一章讲三件事: 连一台服务器有哪两条线路、各自什么时候用; 双方来回的报文长什么样;以及连上之后为什么还要专门打一次招呼。

它在全书链条里的位置: 这是第一章真正动手的内容,也是全书的地基—— 第 05 到 08 章讲的每一件事,都发生在这一章建起来的那条连接上。 同时它也是被 2026 年那次改动动得最狠的一章(本章最后一节会先提个醒)。

1. 服务器不一定跟你在同一台机器上,于是有两条线路

这一节回答:为什么连一台服务器需要先做一个选择。

你可能默认「连服务器 = 连一个网址」。在 MCP 里通常不是。

绝大多数 MCP 服务器是跟你的宿主应用跑在同一台机器上的一个小程序: 你从网上下载它、装在本地、由你的程序自己把它启动起来。 书里说得很明白:大多数商业化的宿主应用,都指望用户自己安装和运行服务器1

但也有另一种情况:你在做一个平台或框架给别人用,你没法保证服务器和应用装在同一台机器上; 或者有的公司把自己的 MCP 服务器架在自己的网络上、对外开放2。这时候就得走网络。

于是有两条线路。选哪条,只取决于一件事:服务器在不在本机。

本机线路远程线路
服务器在哪跟宿主应用同一台机器别人的机器上
谁把它启动的你的客户端亲手启动别人早就开好了
连接靠什么那个程序的输入输出一个网址
书里的评价写起来最省事,当时最常用的一种1平台开发和托管工具的场景2
安全上要注意(第 10 章讲)必须额外做两件事(第 10 章讲)

2. 本机线路:客户端亲手把服务器启动起来

这一节是本章主走查的第 1 步。 我们接一台计算器服务器—— 书里从这里一直用到第 06 章的那台,它提供加、减、乘、除四个工具3

你要交出去的三样东西

要启动一台本机服务器,你的客户端需要知道三件事4。其中第三件叫环境变量——操作系统在启动一个程序时塞给它的一组「名字 = 值」:程序跑起来之后读得到,但它们不写在代码里

下面表里的开关名 --debug 和那个环境变量的值,是我们为演示编的,不是书里的数值; 书里给的参数例子是 ["server.py", "--port", "8080"] 三样东西各是什么,以及「一股脑传全部环境变量不推荐」这条,都是书里的。

交什么这台计算器的例子说明
用什么命令python通常是 pythonnode(有的装法是 npx)
跟哪些参数["calculator_server.py", "--debug"]至少要有服务器文件的路径;后面跟它自己的开关
一份环境变量{"CALC_PRECISION": "8"}见下

第三样要单独说一句:密钥、口令、数据库地址这类不该进代码的东西,通常就放在环境变量里。

配套还有一个约定俗成的做法:把这些「名字 = 值」写进一个叫 .env 的文件里, 程序启动时读进来5这个文件千万不能提交到代码仓库—— 书里在这里插了一条警告,而这条警告正是第 10 章的起点。

还有一个更省事、也更危险的写法:把你自己这台机器上所有的环境变量一股脑传过去。 书里明写不推荐,理由是「这可能把敏感信息暴露给服务器」4。 (为什么这句话比它看起来严重得多,第 10 章讲。)

客户端做的第一件事:把它跑起来

把这三样交给客户端之后,连接的第一步是:客户端执行那行命令, 于是本机上多出来一个正在运行的程序。

这种「由另一个程序启动、并归它管」的程序,叫子程序—— 你的客户端是它的家长:客户端退出的时候,要负责把它也关掉。

启动之后,客户端手里拿到的是两根管子:一根往里写、一根从里读。 这正是这条线路名字的由来——标准输入输出(每个命令行程序天生就有的 「从哪儿读输入、往哪儿写输出」这两条通道),行话简称 stdio6

你的宿主应用

│ ① 执行:python calculator_server.py --debug

┌─────────────────────────┐
│ 子程序:计算器服务器 │
│ │
│ ← 往它的输入里写请求 │
│ → 从它的输出里读回复 │
└─────────────────────────┘

图说:两根管子就是全部的通信手段。没有端口,没有网络,
别的程序也插不进来——这是这条线路天然的一个好处。

3. 远程线路:一个地址、两种回话方式

这一节讲另一条线路,以及它一个容易误解的地方。

远程线路的名字叫 Streamable HTTP(可流式的 HTTP)。 它跟普通网页请求最像,但有一点不同,值得看清楚:

它只有一个地址。 不是「查工具去这个网址、调工具去那个网址」—— 所有来往都发到服务器给你的那一个地址7

而服务器回话有两种方式:

方式客户端怎么发服务器怎么回用在什么时候
一次答完发一条请求过去立刻回一条完整的答复,连接就断绝大多数情况
开一条长连接慢慢吐发一条空请求过去,把连接挂着有话就往这条连接上吐一条,连接一直开着服务器要主动告诉你点什么的时候

第二种是可选的,而且是双向可选的:客户端得主动去请,服务器也可以选择不提供7。 书里专门为此加了一条提醒——因为很多人以为这条长连接是必备的8

记住「服务器可以选择不给」这半句。 第 08 章讲的进度、以及服务器自己那些运行记录,都要靠它送回来; 而第 12 章会告诉你,这条长连接后来被整个换掉了

4. 报文长什么样:一问一答的最小格式

这一节回答:双方来回的到底是什么东西?要不要先学一套新写法?

不用。 双方来回的东西都是用一种通用写法写的,这种写法叫 JSON—— 用一对大括号把内容包起来,里面一格一格排,每格写成 "名字": 值

你在第 2 节已经见过它一次:那份环境变量 {"CALC_PRECISION": "8"} 就是 JSON—— 大括号里一格,名字是 CALC_PRECISION,值是 "8"本章后面出现的每一条报文,写出来也都是这个样子,只是格子多几个。

而这套报文的规矩本身不是 MCP 发明的,它叫 JSON-RPC—— 「用 JSON 写的远程调用」:一条消息说清「我要调哪个方法、带什么参数」, 对方按同一套写法把结果回过来。它比 MCP 老得多。

书里提到它的时候是顺带的:服务器发过来的那种不必回话的消息, 在工具包里就叫 JSONRPCNotification,是 JSONRPCMessage 的一种实现9

最小的一问一答长这样(下面这两条是我们照官方规范写的示例):

客户端发出去 → { "jsonrpc": "2.0",
"id": 1, ← 编号,用来配对
"method": "tools/list", ← 要干什么
"params": {} } ← 带什么参数

服务器回过来 ← { "jsonrpc": "2.0",
"id": 1, ← 同一个编号
"result": { "tools": [ … ] } }

图说:靠 id 配对,所以你可以同时发出好几条不等着,回来的时候按编号认领。
还有第三种消息:**不带 id 的**——不带编号就不必回话,发出去就完事。

认出这三样就够了:方法名(要干什么)、参数、以及那个用来配对的编号。 后面每一章讲的「列清单」「调工具」「读数据」,全都是换一个方法名而已。

5. 打招呼:双方先对一遍版本和能耐

这一节是本章主走查的第 2 步,也是全书最容易被跳过、却被 2026 年改动删掉的一步。

子程序起来了,两根管子也有了。能直接调工具吗?不能。

中间还要建一样东西:会话——一次连接期间双方共用的那份底账 (说到哪儿了、对方支持什么、这次连接的编号是多少)。 在官方 Python 工具包里,它就是那个 ClientSession 对象, 建在上一步那两根管子上面10

会话建好之后,要先打一次招呼:双方各报一遍自己的版本和能耐,对得上才算连上。 (官方文档把这一步叫 initialize,对照表见总纲。)

书里把这一步做了哪四件事列得很清楚,initialize() 负责11:

  • 发起到服务器的连接;
  • 向服务器宣告客户端提供哪些能力;
  • 检查服务器支持的协议版本;
  • 告诉服务器客户端已经初始化好了

这一步在线上一共来回三条报文(下面报文里的具体值是我们照官方规范写的示例; 书里没有给出报文内容):

第几条谁发里面写着什么
客户端 → 服务器method: "initialize",protocolVersion: "2025-06-18",capabilities: {}(我能提供什么),clientInfo: {name: "calculator_server_connection", version: "1.0.0"}
服务器 → 客户端同一个编号,result 里回 protocolVersioncapabilities: {tools: {}}(我支持工具)、serverInfo
客户端 → 服务器method: "notifications/initialized",没有编号——发出去就完,不等回话

对照一下:一次普通的工具调用只要两条报文(一问一答)。 打招呼要三条,多出来的那一条正是「我这边好了」的确认。

书里把整条连接流程总结成四步,和上面完全对得上12:

① 把命令、参数、环境变量装进一个「启动参数」对象
② 把服务器当子程序拉起来,拿到两根管子
③ 在两根管子上建会话
④ 跑一次打招呼

图说:①②是第 2 节,③④是本节。四步做完,`_connected` 才置为真。

不打招呼就调工具会怎样?会被拒。 服务器那一侧根本不知道你说的是哪个版本的规矩, 也不知道该不该把某些能力开给你。

6. 断开:为什么关连接比开连接更容易写错

这一节回答:为什么关连接不是「杀掉那个进程」就完事。

因为一次连接叠了两层:

外层:会话 ← 后建的
────────────
内层:子程序 ← 先起的

图说:必须按相反顺序拆——先收会话,再收子程序。
顺序反了,会话还在往一个已经死掉的程序里写东西。

书里那个写法之所以能一次拆干净,是因为它把两层都压进了同一个里: 建的时候一层一层压上去,拆的时候调一次收尾方法,栈会自动按相反顺序全部弹掉13

作者选这个写法还有第二个理由,值得记住:它让你自己决定什么时候拆, 而不是被代码块的作用域绑死14。连接要跨函数、跨请求活着,这一点是必需的。

7. 边界:2026 年的规范把这次打招呼整个删了

这一节先提个醒,详细的推导在第 11 章。

本章第 5 节那三条报文,在 2026-07-28 那版规范里已经不存在了。 新的做法是:取消打招呼,让每一条请求自己带上协议版本和客户端能耐15

理由一句话说不清——它是从「远程服务器要同时开好几份副本」这件事一级一级推出来的, 中间还牵连到断线之后怎么接上、以及服务器主动发问。整条推导在第 11 章。

在此之前,你只需要记住两件事:

  1. 书里这套写法仍然被支持,规范把它称为「旧的那一代」,并规定了双方怎么互相识别;
  2. 本章第 3 节那条「长连接」也被换掉了,换成了另一种形状(第 12 章)。

另外,书里说两条线路的内部构造「第 5 章会讲」——第 5 章不存在2。 所以本章只讲了作为使用方你需要知道的部分,没有讲线路本身怎么实现。

8. 可带走的

  1. 选哪条线路只看一件事:服务器在不在本机;本机走 stdio,远程走 Streamable HTTP;
  2. 本机线路下,服务器是你的客户端亲手拉起来的子程序——你是它的家长,得负责关掉它;
  3. 启动它要交三样东西:用什么命令、跟哪些参数、一份环境变量;
  4. 环境变量 = 操作系统启动程序时塞给它的「名字 = 值」,不写在代码里;常放在 .env 文件,绝不能提交到仓库;
  5. 把本机所有环境变量一股脑传过去,书里明写不推荐(第 10 章讲后果);
  6. 远程线路只有一个地址;服务器可以一次答完,也可以开一条长连接慢慢吐,后者双方都可以不要;
  7. 报文格式是现成的 JSON-RPC:方法名 + 参数 + 编号;不带编号的那种不必回话;
  8. 连上之后还要打一次招呼,三条报文对一遍版本和双方能耐,不打就调工具会被拒;
  9. 一次连接叠了两层(子程序 + 会话),拆的时候必须按相反顺序;
  10. 打招呼这一步在 2026 年的规范里已被取消,理由见第 11 章。

9. 原文地图

主题原书章原文位置
线路是什么、两种内置实现Initializing the Client and Connecting to a Servertext/07-fm-initializing-the-client-and-connecting-to-a-serv.txt:5(搜「transport is an implementation」) · :9(搜「stdio and streamable HTTP」)
stdio 适用场景、最常用Initializing the Client and Connecting to a Servertext/07-fm-initializing-the-client-and-connecting-to-a-serv.txt:13(搜「common transport, as it is simple to write for」) · :15(搜「run servers themselves」)
远程线路适用场景Initializing the Client and Connecting to a Servertext/07-fm-initializing-the-client-and-connecting-to-a-serv.txt:28(搜「best」) · :36(搜「hosting MCP servers on their own network」)
服务器被当子程序启动Initializing the Client and Connecting to a Servertext/07-fm-initializing-the-client-and-connecting-to-a-serv.txt:59(搜「server as a subprocess」)
命令、参数、环境变量Initializing the Client and Connecting to a Servertext/07-fm-initializing-the-client-and-connecting-to-a-serv.txt:90(搜「identifies the executable」) · :99(搜「environment variables」)
.env 与不要提交Example: A Simple Host Applicationtext/06-fm-example-a-simple-host-application.txt:50(搜「KEY=VALUE」) · :54(搜「Do not commit your」)
远程线路:一个地址、两种回话Initializing the Client and Connecting to a Servertext/07-fm-initializing-the-client-and-connecting-to-a-serv.txt:221(搜「single endpoint defined by the server」)
长连接是可选的Initializing the Client and Connecting to a Servertext/07-fm-initializing-the-client-and-connecting-to-a-serv.txt:230(搜「getting an SSE」)
打招呼做了哪四件事Initializing the Client and Connecting to a Servertext/07-fm-initializing-the-client-and-connecting-to-a-serv.txt:186(搜「advertising the client」)
连接四步小结Initializing the Client and Connecting to a Servertext/07-fm-initializing-the-client-and-connecting-to-a-serv.txt:191(搜「when writing the connection code」)
计算器服务器与它的四个工具Initializing the Client and Connecting to a Servertext/07-fm-initializing-the-client-and-connecting-to-a-serv.txt:320(搜「calculator tools」)
报文类型的名字Interacting with MCP Server Capabilitiestext/08-fm-interacting-with-mcp-server-capabilities.txt:23(搜「JSONRPCNotification」)

Footnotes

  1. 出处:「Initializing the Client and Connecting to a Server」第 13 段(text/07-fm-initializing-the-client-and-connecting-to-a-serv.txt:13,搜「common transport, as it is simple to write for」)。原文给的两条理由是:写起来简单,而且大多数商业化的宿主应用都指望用户自己安装和运行服务器。 2

  2. 出处:「Initializing the Client and Connecting to a Server」第 37 段(text/07-fm-initializing-the-client-and-connecting-to-a-serv.txt:37,搜「hosting MCP servers on their own network」)。同一节里作者两次说两条线路的架构细节「会在第 5 章讲」(text/07-fm-initializing-the-client-and-connecting-to-a-serv.txt:26,搜「explored in more detail in chapter 5」)——第 5 章在这一版里不存在。 2 3

  3. 出处:「Initializing the Client and Connecting to a Server」第 320 段(text/07-fm-initializing-the-client-and-connecting-to-a-serv.txt:320,搜「calculator tools」)。原文明说这是一台「虚构的」服务器,提供 add_two_numberssubtract_two_numbersmultiply_two_numbersdivide_two_numbers 四个工具。启动它的那行代码见第 326 段(text/07-fm-initializing-the-client-and-connecting-to-a-serv.txt:326,搜「calculator_server.py」)。

  4. 出处:「Initializing the Client and Connecting to a Server」第 90 段(text/07-fm-initializing-the-client-and-connecting-to-a-serv.txt:90,搜「identifies the executable」)与第 99 段(text/07-fm-initializing-the-client-and-connecting-to-a-serv.txt:99,搜「environment variables」)。原文举的参数例子是 ["server.py", "--port", "8080"];关于一股脑传全部环境变量,原文的原话是「这样做并不推荐,因为它可能把敏感信息暴露给服务器」。 2

  5. 出处:「Example: A Simple Host Application」第 50 段(text/06-fm-example-a-simple-host-application.txt:50,搜「KEY=VALUE」)。原文说这让你可以把接口密钥这类敏感信息放在代码之外、一个按惯例叫 .env 的文件里。紧跟着的警告见第 54 段(text/06-fm-example-a-simple-host-application.txt:54,搜「Do not commit your」)。

  6. 出处:「Initializing the Client and Connecting to a Server」第 59 段(text/07-fm-initializing-the-client-and-connecting-to-a-serv.txt:59,搜「server as a subprocess」)。这里要提醒一句:原文这一句的读写方向写反了——它说「从服务器的标准输入读消息、通过它的标准输出发消息」,而实际上是反过来的(客户端往子程序的标准输入写、从它的标准输出读)。这是早期试读版里的笔误,不影响机制本身。

  7. 出处:「Initializing the Client and Connecting to a Server」第 221 段(text/07-fm-initializing-the-client-and-connecting-to-a-serv.txt:221,搜「single endpoint defined by the server」)。原文的说法是:回复既可以是立刻返回的普通 HTTP 响应(用 POST 触发,不需要一直开着连接),也可以是流式响应(用一个空的 GET 触发),后者是可选的,取决于服务器的实现 2

  8. 出处:「Initializing the Client and Connecting to a Server」第 230 段(text/07-fm-initializing-the-client-and-connecting-to-a-serv.txt:230,搜「getting an SSE」)。原文特意模拟了一个读者的疑问(「我以为它取代了旧的那套」),然后回答:关键差别就在于流式响应是可选的——客户端要主动请求,服务器也可以选择不支持。这条长连接在 2026-07-28 那版规范里被 subscriptions/listen 取代,见第 12 章。

  9. 出处:「Interacting with MCP Server Capabilities」第 23 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:23,搜「JSONRPCNotification」)。补充(不在书里):JSON-RPC 2.0 是一份独立于 MCP 的老规范,MCP 只是拿它当承载格式。请求带 id、回复用同一个 id 配对、不带 id 的消息不需要回复——这三条都是 JSON-RPC 自己的规定。来源:MCP 官方架构总览 https://modelcontextprotocol.io/docs/2026-07-28/learn/architecture(查阅于 2026-08-25)。

  10. 出处:「Initializing the Client and Connecting to a Server」第 140 段(text/07-fm-initializing-the-client-and-connecting-to-a-serv.txt:140,搜「ClientSession」)与第 197 段(text/07-fm-initializing-the-client-and-connecting-to-a-serv.txt:197,搜「start the MCP client session」)。原文的顺序是:先把两根管子解包成读流和写流,再用这两个流去建会话。

  11. 出处:「Initializing the Client and Connecting to a Server」第 186 段(text/07-fm-initializing-the-client-and-connecting-to-a-serv.txt:186,搜「advertising the client」)。原文原样列的四件事是:发起到服务器的连接、向服务器宣告客户端的能力、检查服务器支持的协议版本、告诉服务器客户端已成功初始化。

  12. 出处:「Initializing the Client and Connecting to a Server」第 191 段(text/07-fm-initializing-the-client-and-connecting-to-a-serv.txt:191,搜「when writing the connection code」)。补充(不在书里):三条报文的具体字段(initialize 请求里的 protocolVersioncapabilitiesclientInfo,以及那条 notifications/initialized)取自官方规范里的报文示例。来源:MCP 官方规范 https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http(查阅于 2026-08-25)。

  13. 出处:「Initializing the Client and Connecting to a Server」第 203 段(text/07-fm-initializing-the-client-and-connecting-to-a-serv.txt:203,搜「to close all server connections in」)。原文的断开写法只有一句:调一次收尾方法,把所有连接按顺序关掉,再把已连接标记置为假、会话置空。

  14. 出处:「Initializing the Client and Connecting to a Server」第 166 段(text/07-fm-initializing-the-client-and-connecting-to-a-serv.txt:166,搜「AsyncExitStack」)。原文强调的两个好处是:可以动态往栈上加东西;以及由你决定什么时候把栈拆开,而不是依赖代码离开某个作用域。

  15. 补充(不在书里):2026-07-28 版规范的变更日志第 2 条写明:移除 initialize / notifications/initialized 握手,改由每条请求在 _meta 里自带协议版本(io.modelcontextprotocol/protocolVersion)与客户端能耐(io.modelcontextprotocol/clientCapabilities);版本对不上时返回 UnsupportedProtocolVersionError。旧写法被称为 legacy(旧的那一代),规范专门规定了双代如何互相识别。来源:MCP 规范变更日志 https://modelcontextprotocol.io/specification/2026-07-28/changelog 与版本兼容页 https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning(均查阅于 2026-08-25)。