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

一个能用的 MCP 服务器三十行就够:一份工具清单、一个发送函数、一个按方法名分支的循环。 但那三十行里一半的坑都在协议细节上:标准输出只能放协议、每行都要刷新、id 要原样带回。

📝 本章和前面几章的关系

上一节连的是别人写好的服务器,这一节自己写一个。 这一节有真跑的服务器代码——它在演示里起过、收过四帧请求、回过四帧回应。

一、服务器一共几块

工具清单:名字、说明、参数 schema。tools/list 时原样返回。 发送函数:把对象序列化成一行 JSON,写到标准输出并刷新。 循环:逐行读标准输入,按 method 分支,调对应的处理函数。

三块之外没有别的。没有框架、没有依赖、没有生命周期管理——因为协议只要求这么多。 这也解释了为什么 MCP 服务器能几十行写完:它不需要成为一个「应用」,只需要成为一个「应答机」。

二、真跑四帧

演示里四帧:initialize、tools/list、两次 tools/call——一次 echo、一次 now。 每一帧的 id 都对上,返回内容都在 result 里。 这就已经是一个能用的服务器了:宿主能连上、能看到工具、能调。

模板可以从这儿开始:先跑通这四帧,再往里加业务。反过来先写业务,会卡在协议上动不了。

三、三个坑

第一个:标准输出只能放协议。任何调试输出都会破坏一行一个 JSON 的约定,调试信息必须走标准错误。 第二个:每写一行都要刷新缓冲。不刷新,客户端会一直等,看起来像卡死。 第三个:id 要原样带回去。忘了带,客户端认不出这是哪次请求的回应。

三个坑全是协议细节,不是业务逻辑。写服务器的麻烦一半都在这里,而且它们都不会给出有用的报错。

四、从三十行到一个能用的服务器

还要补的:参数校验(缺了必填参数要报 -32602)、超时(长任务要能被打断)、日志(写文件, 不能写标准输出)。 再往上才是「工具怎么切」:一个工具查全部,还是五个工具各查一部分。 这个设计决定和 3.10 节的四种模式是一路的:先定动作形状,再写实现。

顺序反了会很别扭:先写五个工具,再发现该合成一个,前面写的全要拆。

五、技术门槛低,设计门槛不低

三十行就能跑,但「该暴露哪几个工具、每个工具的参数怎么收窄」得想很久。 判据还是那三条:工具够不够少、schema 够不够短、权限够不够窄。 写服务器的人最容易犯的错是「把内部 API 一比一暴露出去」。

内部 API 是按人设计的——人知道上下文、会看文档、出错会问。模型不会。 所以给模型的工具要重新设计,不是搬运。

六、动手 Demo:从零写一个服务器:三十行、两个工具、四帧对话

这一段代码可以直接落盘跑。它做的就是三个方法的分支,没有框架、没有依赖。 演示里真起了这个服务器,走了四帧往返。

def send(obj):
    sys.stdout.write(json.dumps(obj, ensure_ascii=False) + u"\n")
    sys.stdout.flush()          # 不刷新,客户端会一直等

for line in sys.stdin:
    req = json.loads(line)
    {"initialize": on_init, "tools/list": on_list, "tools/call": on_call}[req["method"]](req)

实跑输出:

--------------------------------------------------------------------
从零写一个服务器:三十行、两个工具、四帧对话
--------------------------------------------------------------------

  这一段代码可以直接落盘跑。它做的就是三个方法的分支,没有框架、没有依赖。

  ① 服务器一共几块
  --------------------------------------------------------------------
| 块 | 内容 | 什么时候用 |
|---|---|---|
| 工具清单 | 名字、说明、参数 schema | tools/list 时返回 |
| 发送函数 | 写一行 JSON 到标准输出并刷新 | 每次回应都用它 |
| 循环 | 逐行读标准输入、按方法名分支 | 整个服务器的骨架 |


  ② 真跑四帧
  --------------------------------------------------------------------
| 方法 | 请求 id | 回应 id | 返回(截断) |
|---|---:|---|---|
| initialize | 1 | 1 | {"protocolVersion": "2025-06-18", "serve |
| tools/list | 2 | 2 | {"tools": [{"name": "echo", "description |
| tools/call | 3 | 3 | {"content": [{"type": "text", "text": "你 |
| tools/call | 4 | 4 | {"content": [{"type": "text", "text": "2 |


  ③ 三个坑
  --------------------------------------------------------------------
  第一个:标准输出只能放协议。任何调试输出都会破坏一行一个 JSON 的约定——调信息必须走标准错误。
  第二个:每写一行都要刷新缓冲。不刷新,客户端会一直等,看起来像卡死。
  第三个:id 要原样带回去。忘了带,客户端认不出这是哪次请求的回应。
  三个坑全都是「协议细节」,不是业务逻辑。写服务器的麻烦一半都在这里。

  ④ 从三十行到一个能用的服务器
  --------------------------------------------------------------------
  还要补的:参数校验(缺了必填参数要报 -32602)、超时(长任务要能被打断)、日志(写文件,
  不能写标准输出)。
  再往上才是「工具怎么切」:一个工具查全部,还是五个工具各查一部分。
  这个设计决定和 3.10 节的四种模式是一路的:先定动作形状。
  ! 一句话:MCP 服务器的技术门槛很低,设计门槛不低。
  三十行就能跑,但「该暴露哪几个工具、每个工具的参数怎么收窄」得想很久。

①服务器只有三块:工具清单(名字、说明、参数 schema)、发送函数(写一行 JSON 并刷新)、 按方法名分支的循环。三块之外没有别的。 ②真跑四帧:initialize、tools/list、两次 tools/call。 每一帧的 id 都对上,返回内容都在 result 里,这就已经能用了。 ③三个坑全是协议细节:标准输出只能放协议(调试走标准错误)、每写一行都要刷新缓冲、id 要原样带回去。 这三个坑都不会给出有用的报错,现象只是「连不上」或者「卡住」——所以写服务器一半的麻烦在这儿。 ④还要补的:参数校验(缺必填报 -32602)、超时(长任务要能被打断)、日志(写文件,不能写标准输出)。 再往上才是「工具怎么切」。

30 秒实验

  1. 把刷新缓冲那行(flush)注释掉,重跑一遍。客户端会怎样?这个现象和「服务器崩了」能区分开吗?
  2. 在服务器里加一行 print 打印请求,重跑一遍。客户端还能解析吗?为什么?
  3. 给 echo 工具加一个必填参数,故意不传,重跑一遍。你自己写的服务器会报 -32602 吗? 不报的话,客户端看到的是什么?

常见误区

  1. 在服务器里用 print 调试。标准输出是协议通道,print 会把 JSON 冲掉,整条连接失效。
  2. 写完不刷新缓冲。客户端一直等,看起来像卡死,而且报错信息里什么线索都没有。
  3. 回应不带 id 或者乱填。客户端认不出这是哪次请求的回应。
  4. 把内部 API 一比一暴露成工具。内部 API 是按人设计的,给模型的工具要重新切。
  5. 先写业务逻辑,最后才补协议。会卡在协议上动不了,因为业务没法单独验证。
自测三问(点开对答案) 1. 一个能用的 MCP 服务器最少要哪几块? —— 工具清单、一个写一行 JSON 并刷新的发送函数、一个按方法名分支读标准输入的循环。 2. 为什么调试信息不能写标准输出? —— 因为标准输出是协议通道,必须保持一行一个 JSON。 混进别的东西,客户端解析就失败,整条连接失效。 3. 写服务器最容易犯的设计错误是什么? —— 把内部 API 一比一暴露成工具。内部 API 是按人设计的,给模型的工具要按「动作形状」重新切。

自己写的服务器能跑了,下一节退一步看生态:现成的一堆服务器,装之前该算什么账。

相关笔记