🐎 第6章 海纳百川:MCP
📄 一句话概括

MCP 的协议本身很朴素:一行一个 JSON,id 配对,方法名认动作。 三帧就能转起来——握手、列工具、调工具。 出错也走同一套格式。工具不存在和参数错是两个定死的错误码,客户端不用猜,看码就知道改哪儿。

📝 本章和前面几章的关系

上一节算的是「为什么要有标准」,这一节看标准本身长什么样。 这一节有真实往返:起了一个 MCP 服务器子进程,用管道跟它说了五帧话。

一、两边是谁

客户端:宿主里的那一半。负责起进程或者建连接、发请求、收回应、把结果交给模型。 服务器:工具那一半。负责声明自己有哪些工具、执行、返回。 一句话分工:客户端管「问」,服务器管「答」。

模型看到的工具清单,不是模型自己想出来的,是客户端从服务器那儿问来、再塞进上下文的。 记住这一点很要紧:模型从来没「看见」过服务器,它看见的只是一段描述。

二、一行一个 JSON

传输层上跑的是 JSON-RPC 2.0:每条消息一行 JSON,换行结束。 请求有三个必填:jsonrpc 版本、id、method。回应把 method 换成 result 或者 error,id 原样带回。 「一行一个」这个约束很关键:读一行就能解一条,不用等整个包到齐。

这让流式解析变得简单,也让「同一连接上多个请求同时在飞」成为可能——只要 id 不撞。

三、三个方法就能转起来

initialize 交换版本和能力;tools/list 列出工具;tools/call 调一次。 演示里三帧请求、三帧回应,id 一一对上,返回内容都在 result 里。 这三帧是一切 MCP 会话的骨架。少了握手,后面两个都不认;少了列工具,模型不知道有什么可调。

所以写服务器的时候,最省事的做法就是先只实现这三个方法——它已经能用了。

四、错误也是协议的一部分

-32601 是「方法不存在」,-32602 是「参数不对」。这两个码是 JSON-RPC 规范里定死的。 演示里发了一个不存在的方法、一个不存在的工具,服务器各回了一个错误帧,id 照样对上。 所以「出错」不是异常路径,是正常路径的一部分。

客户端不用猜「是网络问题还是我写错了」,看码就知道该改哪儿。这一条能省掉大量排查时间。

五、为什么这层要单独讲

因为它决定了后面所有事的形状:传输是换载体、配置是换入口、权限是在哪一层拦。 而它本身简单到几乎没有设计空间——这正是标准该有的样子:无聊、稳定、够用。 反过来说,如果一层协议让你觉得很花哨,那多半是它想解决的问题还没想清楚。

记住一句话:MCP 不神秘,它就是一行一个 JSON 的问答。难的不是协议,是谁在什么权限下能调哪些工具。

六、动手 Demo:起一个真的 MCP 服务器,走一遍握手、列工具、调工具

MCP 就是一段 JSON-RPC:一行一个 JSON,id 配对,方法名认动作。 这里真起一个子进程当服务器,用管道跟它说话。五帧全是真实往返。

def _call(p, obj):
    p.stdin.write((json.dumps(obj) + u"\n").encode())
    p.stdin.flush()
    return json.loads(p.stdout.readline())

实跑输出:

--------------------------------------------------------------------
起一个真的 MCP 服务器,走一遍握手、列工具、调工具
--------------------------------------------------------------------

  MCP 就是一段 JSON-RPC:一行一个 JSON,id 配对,方法名认动作。
  这里真起一个子进程当服务器,用管道跟它说话。

  ① 三帧请求,三帧回应
  --------------------------------------------------------------------
| 方法 | 请求 id | 回应 id | 内容(截断) |
|---|---:|---|---|
| initialize | 1 | 1 | {"protocolVersion": "2025-06-18", "s |
| tools/list | 2 | 2 | {"tools": [{"name": "read_note", "de |
| tools/call | 3 | 3 | {"content": [{"type": "text", "text" |


  ② 出错的时候协议长什么样
  --------------------------------------------------------------------
| 方法 | 请求 id | 错误码 | 错误信息 |
|---|---:|---|---|
| tools/call | 4 | -32602 | unknown tool: no_such_tool |
| resources/list | 5 | -32601 | method not found |


  ③ 三个方法就够转起来了
  --------------------------------------------------------------------
  initialize 交换版本和能力、tools/list 列出工具、tools/call 调一次。这三帧是一切 MCP 会话的骨架。
  注意回应里的 id 跟请求的 id 一模一样——这就是「配对」。
  一个连接上可以同时有多个请求在飞,靠 id 认领各自的结果。

  ④ 错误码是协议的一部分
  --------------------------------------------------------------------
  -32601 是「方法不存在」,-32602 是「参数不对」。这两个是 JSON-RPC 规范里定死的。
  所以工具不存在和参数错,客户端不用猜,看码就知道该怎么改。
  ! 一句话:MCP 不神秘,它就是一行一个 JSON 的问答。
  难的不是协议,是「谁在什么权限下能调哪些工具」——那是后面 6.8 节的事。

①三帧请求、三帧回应,id 一一对上。 initialize 换版本和能力、tools/list 列工具、tools/call 调一次,这三帧是一切 MCP 会话的骨架。 ②回应里的 id 跟请求的 id 一模一样,这就是「配对」。 一个连接上可以同时有多个请求在飞,靠 id 认领各自的结果。 ③错误也是协议的一部分:-32601 是方法不存在,-32602 是参数不对,这两个码是 JSON-RPC 规范里定死的。 客户端不用猜,看码就知道该怎么改。 ④传输层上是「一行一个 JSON」,读一行就能解一条,不用等整个包到齐。 这个约束让流式解析变简单,也让同一连接上多请求并行成为可能。

30 秒实验

  1. 再发一帧 tools/call,把参数故意写错(缺必填字段),看服务器回什么。 它的错误码和 -32602 是同一个吗?说明什么?
  2. 把请求里的 id 改成字符串(原来用整数),重跑一遍。还配对得上吗?这说明 id 的类型有规定吗?
  3. 连着发两帧请求,先不读回应,再把两条回应一起读。顺序一定是先发先回吗?靠什么认领?

常见误区

  1. 以为 MCP 是个复杂框架。它就是 JSON-RPC,三帧能跑通,三十行能写一个服务器。
  2. 回应里不带 id 或者乱填 id。客户端认不出这是哪次请求的回应,整条连接就乱了。
  3. 把错误帧当异常吞掉。错误是协议的一部分,吞了客户端只能等超时,排查时什么线索都没有。
  4. 在协议层上做业务判断。协议只管问答格式,权限和收窄是工具侧的事。
  5. 以为模型能直接看见服务器。它看见的只是客户端塞进上下文的一段工具描述。
自测三问(点开对答案) 1. MCP 的三帧骨架是哪三个方法? —— initialize 交换版本和能力、tools/list 列出工具、tools/call 调一次。 2. 为什么 id 必须原样带回? —— 因为一个连接上可以同时有多个请求在飞,客户端靠 id 认领哪个回应属于哪次请求。 3. -32601 和 -32602 分别是什么意思? —— -32601 是方法不存在,-32602 是参数不对。两个都是 JSON-RPC 规范里定死的错误码。

协议讲完了,下一节看它可以架在什么上面:三种传输,差别在连接能活多久。

相关笔记