跳到主要内容

徒手实现一遍协议 — JSON-RPC、STDIO 与握手

这一章讲三件事: MCP 线上跑的到底是什么格式的消息; 「传输」这一层长什么样、为什么换传输不用改协议; 以及客户端和服务器正式通话之前,那套一步都不能少的握手。 原书的教法很特别:不画架构图,而是带你用几十行 TypeScript 把协议亲手实现一遍—— 这一章是全书的技术地基,后面所有 SDK 用法都是它的「自动挡」版本。

1. 顶层全景:两个进程,一种消息,三步开场

先把这一章的全部内容压成一张图:

客户端(你的程序) 服务器(能力提供方)
───────────── ─────────────────
spawn 启动 ───────────────────────► 服务器进程诞生,开始按行读 stdin
① initialize 请求 ────────────────► 「你会什么?」
◄─────────────── ② capabilities 回应 「我会 tools/resources/prompts」
③ notifications/initialized ──────► 「准备好了,开工」
④ tools/list、tools/call … ◄─────► 正常的请求-响应
⑤ notifications/progress ◄──────── 单向通知:没人需要回答

图说:所有往来的文字都是 JSON-RPC 消息;①②③ 是握手,
握手没完成前发 ④,服务器应该回错误。

这条「从启动到调用工具」的完整链路就是本章主走查。 下面每一节填充它的一个环节:消息格式(②节)、传输接口(③节)、 STDIO 怎么跑(④节)、握手(⑤节)、代码怎么写(⑥节)、通知(⑦节)。

2. 消息:MCP 没有发明新格式,它用的是 JSON-RPC

上一章说过,MCP 是一份「约定」。这份约定的第一层是:所有消息都用 JSON-RPC 2.0 格式—— JSON-RPC 是一个比 MCP 老得多的远程调用约定 (远程调用,RPC,就是「这台机器让那台机器执行一个函数」的意思), 它规定一条请求消息长这样1:

{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }

四个字段各管一件事:

字段管什么没有它会怎样
jsonrpc版本标记(一个标明「这句话按哪一版格式写」的字段),固定 "2.0"对方不知道你在说哪种话
id这次请求的编号响应回来了,你不知道在对哪一次请求
method要对方做什么,如 tools/list这就是请求本身
params做这件事要的参数视方法而定,可以为空

id 是理解整个协议的钥匙。 客户端发出 id: 1 的请求, 服务器的回应里也带 id: 1——靠这个号,客户端才能把「回应」和「请求」对上, 尤其是同时有好几个请求在飞的时候。我们协议书架的规范拆解把这叫做 「请求/响应配对」,是 JSON-RPC 这一层最重要的机制2

还有一种消息故意没有 id:通知(notification)—— 一方单方面知会另一方一件事(比如「进度到 50% 了」),不需要、也不允许回答。 第 7 节专门讲它。

3. 传输:协议不管消息怎么运,只管运到了长什么样

消息格式定了,下一个问题是:这些 JSON 文本怎么从客户端手里到服务器手里?

MCP 的答案叫传输(transport)——它规定「怎么运」的那一层, 而且协议本身对传输是中立的:同一套 JSON-RPC 消息, 可以走本地进程的输入输出,也可以走网络上的 HTTP3。 原书给出 SDK(软件开发工具包——别人预先写好、你直接拿来搭自己程序的一包工具) 里传输层的接口定义,所有传输实现都遵守同一个形状4:

interface Transport {
start(): Promise<void>; // 开始收发消息
send(message: JSONRPCMessage): Promise<void>; // 发一条
close(): Promise<void>; // 关闭
onclose?: () => void; // 三个事件回调:
onerror?: (error: Error) => void; // 断了、错了、
onmessage?: (message: JSONRPCMessage) => void; // 来消息了
}

这个接口的价值在于:协议代码只跟这六个成员打交道,底下是管道还是网线,它不关心。接口在这里就是「一组约好了的方法名和形状」,谁实现它,谁就能插进这个位置; 那三个 on 开头的成员叫回调——你事先把一个函数挂上去, 等「断了」「错了」「来消息了」发生时,由传输层反过来调用它。) 我们书架对官方 TypeScript SDK 的拆解证实:真实的 SDK 里确实就是这样一层 Protocol 抽象加 Transport 接口,本书这个简化版形状与真品一致5

4. STDIO 传输:把服务器「生」成自己的子进程

第一种传输叫 STDIO(标准输入输出)—— 每个命令行程序天生就有两根「管子」:stdin(标准输入,程序从这里读) 和 stdout(标准输出,程序往这里写)。你在终端里敲字、看输出,用的就是它们6

MCP 拿这两根管子干了一件很妙的事:客户端把服务器程序启动成自己的子进程—— 子进程就是被另一个程序(父进程)启动、受它看管的一个新程序—— 然后往子进程的 stdin 写消息、从它的 stdout 读回应7。 Node.js 里启动子进程用 spawn:

import { spawn } from 'child_process';
const child = spawn('node', ['server.js']); // 服务器被「生」出来
child.stdin.write("data 1\n"); // 父 → 子:写进它的 stdin
child.stdout.on('data', (data) => { // 子 → 父:从它的 stdout 读
console.log(`(FROM SERVER): ${data}`);
});

服务器那一侧更朴素,用 readline 按行读自己的 stdin, 收到一行就往自己的 stdout 打印一句——它打印的东西,就从管子的另一头流回客户端8。 原书这套教学代码跑起来的真实输出是9:

Client sending data to server...
Client::ondata> (FROM SERVER): line received: (Server data): data 1
EXIT received: Server closing down...
Client::onexit> (FROM SERVER): SERVER exited with code 0

为什么第一台 MCP 服务器都长这样? 因为服务器跑在你自己机器上、 由客户端负责启动和收尾,没有端口、没有防火墙、没有认证—— 这是本地集成成本最低的一种形态。代价是它离不开这台机器,上网是第 04 章的事。

到这里,管子通了,但管子里流的还是随意文本。把它升级成 MCP, 只差一步:让两边流的都是第 2 节那种 JSON-RPC 消息,并且遵守下一节的开场顺序。

5. 握手:三步,一步都不能省

主走查的第 ①②③ 步来了。 连上之后,客户端不能上来就 tools/list。 协议规定了一套开场,书里叫它握手(handshake)——双方正式开工前的互相确认10:

第 ① 步,客户端发 initialize 请求,自报家门外加一句「我会什么」:

{ "jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": { "roots": { "listChanged": true }, "sampling": {} },
"clientInfo": { "name": "ExampleClient", "version": "1.0.0" } } }

这里有两个新东西。一是 protocolVersion:协议版本号,用一个日期当名字, 书里的例子写的是 2024-11-05——就是 2024 年 11 月 5 日那版规范; 双方靠它对齐「我们说的是哪一版的 MCP」11二是 capabilities(能力声明):一张「我会什么」的清单, 客户端声明自己会 rootssampling(这两个是什么,第 08 章讲)—— 声明过的,对方才允许用12

第 ② 步,服务器回应自己的能力清单(注意:回应带着同一个 id: 1):

{ "jsonrpc": "2.0", "id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"logging": {},
"prompts": { "listChanged": true },
"resources": { "subscribe": true, "listChanged": true },
"tools": { "listChanged": true } },
"serverInfo": { "name": "ExampleServer", "version": "1.0.0" } } }

这份清单就是客户端的「菜单」:toolsresourcesprompts 是服务器能提供的三样东西 (第 03 章的主角),logging 表示它可以给客户端发日志消息—— 日志就是程序运行时随手记下的「发生过什么」的文字记录,以后排障全靠它翻案13

第 ③ 步,客户端发 notifications/initialized—— 一条通知(没有 id,服务器不回答它),意思是「我确认了,开工吧」14:

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

规矩是:在这条通知到达之前,客户端发任何别的请求,服务器都应该回错误14。 服务器端的实现就是一个两档开关:initialized 为假时只认 initializenotifications/initialized 两种方法,其余一律报错; 收到后者后把开关拨真,才进入正常服务状态15

6. 把它写成代码:事件和「等一个回答」怎么拼起来

动手写时有一个真实的难点,值得讲透,因为它是每个新手第一次写客户端都会卡住的地方。

听管子是「事件式」的——你不知道服务器的回应哪一秒到,只能挂一个「到了就喊我」的函数; 可写业务逻辑想用的是「等它回来再往下走」——先发 initialize,回应来了,再发下一条。 这两件事怎么拼?

原书的拼法是 JavaScript 里的标准做法,分两步16:

  1. 挂监听时不用 on(一直听),用 once(只听一次)——听到第一条回应就自动摘了, 不会干扰后面别的请求;
  2. 把这个「只听一次」包进一个 Promise—— Promise 是 JavaScript 表示「一件将来的事」的物件,配合 await 就能写出 「等这件事完成再往下走」的代码,把事件式的等待伪装成一步一步的顺序执行。

于是 connect() 的骨架是:发 initialize → 返回一个 Promise → once 听到 initialize 回应 → 补发 notifications/initialized → 装好长期监听器 → Promise 完成。listTools() 同理:发 tools/listonce 等回应 → 把结果交出来。书里用这套代码跑出了完整走查的真实输出17:

DEBUG Client received initialize response: {
protocolVersion: '2024-11-05',
capabilities: { logging: {}, prompts: {…}, resources: {…}, tools: {…} },
serverInfo: { name: 'ExampleServer', version: '1.0.0' } }
DEBUG Client connected and initialized:
Tools response: { tools: [ { name: 'ExampleTool', version: '1.0.0',
description: 'An example tool for demonstration purposes.' } ] }

调工具的 tools/call 走的是同一个模子:请求里 method 换成 tools/callparams 里带工具名和参数;服务器的回应里,真正的内容装在一个文本块数组里—— 书里那次调用 exampleTool、参数 ["arg1","arg2"], 回来的文本块写着 Called tool exampleTool with arguments ["arg1","arg2"]18

7. 通知与其他流:不是所有消息都有回答

主走查的第 ⑤ 步。通知有固定的命名空间—— 命名空间就是一套名字共用的统一前缀,前缀相同的名字算同一族: 所有通知的方法名都以 notifications/ 开头, 常见的有 notifications/progress(进度)和 notifications/cancelled(取消)19。 客户端实现上,书里用了一个简单的分发:看消息有没有 id、方法名以什么开头—— 是 notifications/ 开头就交给通知处理器,有 result 就当请求回应, 以此决定这条消息该进哪条处理通道20

原书在这一章还提前亮了另外两张牌,我们各记一句、细节留到对应章节:

  • 采样(sampling):服务器反过来请求客户端,让客户端的大语言模型帮忙补全一段内容—— 消息方向与「客户端调服务器」相反,是协议里最反直觉的设计,第 08 章整章讲它21;
  • 上网的两种传输:SSE 和 Streamable HTTP。这一章只给了概念对比—— 前者是「一个长连接管推、一个端点管发」,后者是「单一端点全包」, 它们各自怎么做、为什么后者取代了前者,第 04 章整章讲22

8. 作者的判断与证据

有证据的:

  • JSON-RPC 四字段、握手三步、通知无 id,都是规范本身的规定,书里的消息示例与规范一致; 我们书架上的规范拆解(基于 2026-07-28 版)可以逐条对上2;
  • 教学代码的运行输出书里给了全文,数字与结构可核91718

作者的判断(或教学法选择):

  • 用「徒手写一遍」教协议,是作者的教学取舍。他自己在章末承认:这份代码「能跑, 但在性能、可维护性上肯定还能改进」23——它是教具,不是生产代码;
  • 「真实客户端可以只发 initialized 就开工,但先交换 capabilities 是好习惯」10—— 这是作者的经验建议,规范层面握手三步是硬性要求。

判断(我们的,不是书里的): 这一章是全书含金量最高的一章。 多数 MCP 教程从 SDK 的 server.tool() 讲起,读者学会了调用却没见过协议; 这章反着来,先让你把协议的每一颗螺丝摸过一遍,后面再用 SDK 时, McpServerconnect() 在你眼里就不是黑箱,而是「那几十行代码的自动挡」。 如果错,会错在: 如果读者完全不打算自己写客户端或服务器(只用现成宿主), 这章的 ROI 会下降——但即便如此,第 07 章排障那一节会证明:看得懂握手日志,是排查一切 MCP 故障的前提。

9. 边界与局限

  • 书里示例的协议版本是 2024-11-05,后面章节还出现 2025-03-262025-06-18—— 三个日期对应规范的不同版本,书里混用而没有解释版本之间的差别。 补充一个不在书里的事实:规范此后仍在演进,我们的规范拆解基于 2026-07-28 版, 那一版把「握手建立会话(session——一次连接期间,双方各自记住对方状态的那段时间)」 改造成了无状态(stateless——服务器不记任何状态,每个请求自己带全身份信息): 本书描述的「有状态的握手 + 会话」是 2024–2025 年代的协议形态,读的时候要带着这个时间戳——这里是比喻义:一个提醒你「这话属于哪个年代的说法」的印记24;
  • 教学实现有一个简化没有点破:它假设「一条 stdout 数据就是一条完整消息」。 真实实现要处理消息被切成几段到达的情况,SDK 里有专门的帧处理5;
  • 错误处理只演示了「方法不认识就回错」,JSON-RPC 自己的错误码体系(如 -32601 方法未找到)没讲;
  • 安全和认证在这一章完全缺席——那是故意的,STDIO 子进程模型里确实不需要; 到了 HTTP 就是另一回事(第 10 章)。

10. 可带走的

  1. MCP 消息 = JSON-RPC 2.0:jsonrpc/id/method/params 四个字段,id 负责把回应配回请求;
  2. 没有 id 的消息是通知,单向、不许回答;notifications/ 是它的命名空间;
  3. 传输中立:协议只认 Transport 接口(start/send/close + 三个回调),换传输不换协议;
  4. STDIO = 客户端 spawn 服务器、写它 stdin、读它 stdout——本地形态,无网络无认证;
  5. 握手三步:initialize → capabilities 回应 → notifications/initialized; 第三步到达前发别的请求,协议要求报错;
  6. capabilities 是「菜单」:客户端只许用对方声明过的能力;
  7. 事件式监听 + once + Promise,是「发一条、等一条」的标准拼法;
  8. 排障第一招永远是:把线上的 JSON-RPC 消息打出来,对着握手三步看——第 07 章会用到。

11. 原文地图

主题原书章原文位置
JSON-RPC 四字段Explaining the Model Context Protocoltext/04-fm-explaining-the-model-context-protocol.txt:23(搜「jsonrpc」)
Transport 接口同上text/04-fm-explaining-the-model-context-protocol.txt:123(搜「Start processing messages」)
传输中立同上text/04-fm-explaining-the-model-context-protocol.txt:98(搜「transport agnostic」)
STDIO 服务器(readline)同上text/04-fm-explaining-the-model-context-protocol.txt:201(搜「readline.createInterface」)
spawn 客户端同上text/04-fm-explaining-the-model-context-protocol.txt:277(搜「child_process」)
运行输出同上text/04-fm-explaining-the-model-context-protocol.txt:406(搜「Client sending data to server」)
握手三步同上text/04-fm-explaining-the-model-context-protocol.txt:502(搜「initialize」)
初始化响应全文同上text/04-fm-explaining-the-model-context-protocol.txt:596(搜「protocolVersion」)
initialized 前报错同上text/04-fm-explaining-the-model-context-protocol.txt:518(搜「hasn't fully」)
once + Promise同上text/04-fm-explaining-the-model-context-protocol.txt:782(搜「wrapping the stream's callback in a promise」)
完整 client.ts同上text/04-fm-explaining-the-model-context-protocol.txt:913(搜「handleRpcMessage」)
服务器状态机同上text/04-fm-explaining-the-model-context-protocol.txt:1104(搜「let initialized = false」)
tools/call 实现同上text/04-fm-explaining-the-model-context-protocol.txt:1462(搜「tools/call」)
通知类型同上text/04-fm-explaining-the-model-context-protocol.txt:1687(搜「cancelled」)
sampling 概念同上text/04-fm-explaining-the-model-context-protocol.txt:1923(搜「I don't know how to do this」)
SSE 与 Streamable HTTP 概念同上text/04-fm-explaining-the-model-context-protocol.txt:2392(搜「/messages」)· :2458(搜「/mcp」)

Footnotes

  1. 出处:「Explaining the Model Context Protocol」第 21-48 段(text/04-fm-explaining-the-model-context-protocol.txt:21,搜「JSON-RPC specification」;:435,搜「tools/list」)。

  2. 补充(不在书里,依据我们的 protocol 书架):请求/响应配对、通知无 id、错误码与 _meta,都是 JSON-RPC 消息层的规定。 依据: shelf=ai-protocol-reference/mcp-spec#01-jsonrpc-and-messages.md 事实=该章讲 JSON-RPC 消息形态、resultType、错误码与「无状态」改造。 2

  3. 出处:「Explaining the Model Context Protocol」第 98 段(text/04-fm-explaining-the-model-context-protocol.txt:98,搜「transport agnostic」)。

  4. 出处:「Explaining the Model Context Protocol」第 122-133 段(text/04-fm-explaining-the-model-context-protocol.txt:123,搜「Start processing messages」)。

  5. 补充(不在书里,依据我们的 protocol 书架):官方 TypeScript SDK 里是 Protocol 抽象类 + Transport 接口,负责 JSON-RPC 帧、请求/响应配对、超时与取消。 依据: shelf=ai-protocol-reference/mcp-typescript-sdk#02-protocol-and-transport.md 事实=该章讲 Protocol 抽象类与 Transport 接口的职责划分。 2

  6. 出处:「Explaining the Model Context Protocol」第 157-171 段(text/04-fm-explaining-the-model-context-protocol.txt:159,搜「standard input and output」)。

  7. 出处:「Explaining the Model Context Protocol」第 257-277 段(text/04-fm-explaining-the-model-context-protocol.txt:257,搜「child process with the client being the parent」;:277,搜「spawn」)。

  8. 出处:「Explaining the Model Context Protocol」第 199-213 段(text/04-fm-explaining-the-model-context-protocol.txt:201,搜「readline.createInterface」)。

  9. 出处:「Explaining the Model Context Protocol」第 406 段(text/04-fm-explaining-the-model-context-protocol.txt:406,搜「Client sending data to server」)。 2

  10. 出处:「Explaining the Model Context Protocol」第 492 段(text/04-fm-explaining-the-model-context-protocol.txt:492,搜「handshake」)与第 708 段(text/04-fm-explaining-the-model-context-protocol.txt:708,搜「good practice to exchange capabilities first」)。 2

  11. 出处:「Explaining the Model Context Protocol」第 528-545 段(text/04-fm-explaining-the-model-context-protocol.txt:533,搜「protocolVersion」)。

  12. 出处:「Explaining the Model Context Protocol」第 561-582 段(text/04-fm-explaining-the-model-context-protocol.txt:577,搜「capabilities」)。

  13. 出处:「Explaining the Model Context Protocol」第 617-641 段(text/04-fm-explaining-the-model-context-protocol.txt:623,搜「Logging」)。

  14. 出处:「Explaining the Model Context Protocol」第 653-690 段(text/04-fm-explaining-the-model-context-protocol.txt:667,搜「should not produce a response」;:514,搜「should produce an error response」)。 2

  15. 出处:「Explaining the Model Context Protocol」第 1104-1149 段(text/04-fm-explaining-the-model-context-protocol.txt:1104,搜「let initialized = false」)。

  16. 出处:「Explaining the Model Context Protocol」第 782-816 段(text/04-fm-explaining-the-model-context-protocol.txt:782,搜「wrapping the stream's callback in a promise」)。

  17. 出处:「Explaining the Model Context Protocol」第 1420-1442 段(text/04-fm-explaining-the-model-context-protocol.txt:1420,搜「DEBUG Client starting server at」)。 2

  18. 出处:「Explaining the Model Context Protocol」第 1462-1641 段(text/04-fm-explaining-the-model-context-protocol.txt:1462,搜「tools/call」;:1632,搜「Called tool exampleTool」)。 2

  19. 出处:「Explaining the Model Context Protocol」第 1683-1699 段(text/04-fm-explaining-the-model-context-protocol.txt:1687,搜「cancelled」)。

  20. 出处:「Explaining the Model Context Protocol」第 2260-2277 段(text/04-fm-explaining-the-model-context-protocol.txt:2262,搜「startsWith("sampling/")」)。

  21. 出处:「Explaining the Model Context Protocol」第 1913-1945 段(text/04-fm-explaining-the-model-context-protocol.txt:1923,搜「I don't know how to do this」)。

  22. 出处:「Explaining the Model Context Protocol」第 2368-2476 段(text/04-fm-explaining-the-model-context-protocol.txt:2392,搜「/messages」;:2458,搜「/mcp」)。

  23. 出处:「Explaining the Model Context Protocol」第 2494-2498 段(text/04-fm-explaining-the-model-context-protocol.txt:2498,搜「could surely be improved」)。

  24. 补充(不在书里,依据我们的 protocol 书架):2026-07-28 版规范把协议从「有会话握手」改造成「每个请求自带身份的无状态协议」。 依据: shelf=ai-protocol-reference/mcp-spec#index.md 事实=该拆解注明 2026-07-28 版把协议改造成无状态,本文档基于该版。