🔌 第1章 快速了解MCP
📄 一句话概括

同一句「明天杭州下雨吗,下雨就找个室内的地方逛逛」, 接不接 MCP,差别不在答案好不好,在你替几个宿主写了几遍适配。 4 个宿主乘 6 个服务,硬接是 24 份胶水;走 MCP 是 6 个 Server。 这一节把两条路的活各摆一遍,看省下的到底是哪一部分。

📝 本章和前面几章的关系

这是全书第一章。第 2 章拆原理,第 6 章自己写一个 Server,第 8 章往后是九个案例。 现在只需要看懂一件事:同一份能力,为什么换个宿主就得重写一遍。 后面十章都在给这一步添东西——添安全、添调试、添部署。 添完之后回头看这一节,会发现那条主线一直没变。

一、一句话,背后两个服务

需求只有一句:明天杭州会下雨吗?如果下雨,帮我找个室内能逛的地方。 这句话里藏着两个动作:查天气、找地点。真要办成,后面还得有两个接口。 对模型来说,这两个动作它自己做不到——它没有网络,也不认识今天的杭州。

所以一定要有人把查询结果递到它面前,它才能接上下一句。 【问题从来不是「怎么查」,是「谁来接这根线」。】这一节比的就是这个。

二、不用 MCP:先写适配,再写适配

最直接的做法是给每个宿主写一个函数。给 GPT 写的返回字符串,给文心写的返回一个带 text 字段的对象。 两段代码查的是同一个接口、取的是同一个字段,区别只在返回值的包装。 一个宿主一份,看着没什么。可宿主有 4 个、服务有 6 个,这种适配就得写 4 乘 6 等于 24 份。

真正贵的不是写,是改。 六个月后天气接口把字段名换了一个字, 24 份里凡是碰这个字段的都得翻出来改一遍,改漏一份就是一个线上故障。 这就是「不用 MCP」的真实成本:不是难,是散。散在二十几个文件里,没人能一眼看全。

三、用 MCP:写一个 Server,谁都能连

换成 MCP,你只写一个天气 Server。宿主不管是谁,照同一套话术来连。 连上只要四行报文:握手、告知、列工具、调工具。 演示里这四行是真跑出来的,不是抄文档——真起了一个子进程,真把 JSON 写进管道再读回来。 四步之后连接就建好了。换成 4 个宿主,还是这四步;换成 100 个,还是这四步。

注意这里省掉的是什么。 不是省掉了写查询代码——查询代码本来就要写一次。 省掉的是「为第二个宿主再写一遍」这件事。 第 3 步列出来的那张工具表,就是全部家当:名字、说明、参数。 宿主拿到它,就知道该怎么向模型介绍这个能力。

四、差强人意的那一句:这件事我做不了

演示到这里都还顺利。接着用户说了下一句:帮我记一笔账,晚饭 56 块 5。 眼下的 Server 只会查天气、找地点,没有跟记账有关的工具,只能回答「这件事我做不了」。 换成写死的接法,这就是天花板:宿主接了什么,就永久只能做什么。想加一个能力,得改宿主、 重新发版、等用户升级。

换成 MCP,加能力不用动宿主。 Server 那边多挂一个工具,再往外推一条「我的工具表变了」的通知,宿主重新列一次,从 2 个变成 3 个。 同一句话再问一遍,这回有工具接了,回了 total 等于 3。 【用户感知到的差别,就在这一句上】:一个说做不了,一个接着做了。

五、两种接法摆在一起

下表把两条路的六件事逐条对齐。左边那一列不是「落后的做法」,它只是没打算让别人复用。

比什么 不用 MCP 用 MCP
要写的适配 宿主数乘服务数,24 份 服务数,6 个 Server
加一个服务 每个宿主各加一份,4 处 加一个 Server,1 处
加一个宿主 每个服务各接一次,6 处 照协议连,0 处
接口改字段 24 份里挨个找 改 Server 一处
加一个能力 改宿主、发版、等升级 Server 挂上新工具
换模型 适配全重写 一行不动

MCP 没让「调接口」这件事变简单,它让「同一件事不用做第二遍」。 这句要记住,因为绝大多数对 MCP 的误解都出在这儿:以为它是个更强的接口调用框架, 实际上它只是个约定。

六、动手 Demo:同一件事,两种接法

造一句话、两个服务,按两种接法各做一遍。 报文那部分是真实跑出来的:起子进程、写管道、读回一行,没有一处是编的。 只有「下雨了就找室内」这个判断,在真机上由模型做,这里换成一条 if。

import json
import subprocess

# 最小客户端:一根管道,四行报文
p = subprocess.Popen(['python', 'trave.py'], stdin=subprocess.PIPE,
                     stdout=subprocess.PIPE, text=True,
                     encoding='utf-8')


def rpc(method, params, id=None):
    msg = {'jsonrpc': '2.0', 'method': method, 'params': params}
    if id is not None:
        msg['id'] = id
    p.stdin.write(json.dumps(msg, ensure_ascii=False) + '\n')
    p.stdin.flush()
    if id is None:            # 通知:只写不读
        return None
    return json.loads(p.stdout.readline())


rpc('initialize', {'protocolVersion': '2024-11-05',
                   'capabilities': {}, 'clientInfo': {'name': 'demo'}}, 1)
rpc('notifications/initialized', {})
print(rpc('tools/list', {}, 2))
print(rpc('tools/call', {'name': 'add',
                         'arguments': {'a': 2, 'b': 40}}, 3))

实跑输出:

--------------------------------------------------------------------
同一件事,两种接法
--------------------------------------------------------------------

需求只有一句:明天杭州会下雨吗?如果下雨,帮我找个室内能逛的地方。
这句话背后要两个服务:一个查天气、一个查地点。下面按两种接法各做一遍。

  ① 不用 MCP:先写适配
  --------------------------------------------------------------------
  # 给 GPT 写的天气适配
  def get_weather_for_gpt(city, day):
      r = requests.get(WEATHER_API, params={"city": city, "day": day})
      return r.json()["data"]["forecast"][0]["text"]

  # 给文心写的天气适配
  def get_weather_for_ernie(city, day):
      r = requests.get(WEATHER_API, params={"city": city, "day": day})
      return {"text": r.json()["data"]["forecast"][0]["text"]}
两段代码查的是同一个接口、拿的是同一个字段,区别只有返回值的包装。
宿主有 4 个、服务有 6 个,这样的适配就要写 4 乘 6 等于 24 份。

六个月后天气接口改了一个字段名,24 份里凡是碰这个字段的都要改。
这就是「不用 MCP」的真实成本:【不是难,是散。】

  ② 用 MCP:写一个 Server,谁都能连
  --------------------------------------------------------------------
为了能真跑,这里先用一个最小的 Server(两数相加那版,第 6 章自己写一个)。
重点不在它算什么,在于【它怎么被连上】。

连接过程一共四行报文,原文是这样:
  >> {"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05",
     "capabilities":{},"clientInfo":{"name":"demo-client","version":"0.1.0"}},"id":1}
  << {"jsonrpc": "2.0", "id": 1, "result": {"protocolVersion": "2024-11-05", 
     "capabilities": {"tools": {}}, "serverInfo": {"name": "mini", "version": "0.1.0"}}}
  >> {"jsonrpc":"2.0","method":"notifications/initialized"}
  >> {"jsonrpc":"2.0","method":"tools/list","params":{},"id":2}
  << {"jsonrpc": "2.0", "id": 2, "result": {"tools": [{"name": "add", "description": 
     "两数相加", "inputSchema": {"type": "object", "properties": {"a": {"type": "number"}, 
     "b": {"type": "number"}}, "required": ["a", "b"]}}]}}
  >> {"jsonrpc":"2.0","method":"tools/call","params":{"name":"add","arguments":{"a":2,
     "b":40}},"id":3}
  << {"jsonrpc": "2.0", "id": 3, "result": {"content": [{"type": "text", "text": "42"}], 
     "isError": false}}

| 步 | 方向 | 方法 | 回来的是什么 |
|---:|---|---|---|
| 1 | 客户 -> 服务 | initialize | 服务端说自己是谁、会说哪版 |
| 2 | 客户 -> 服务 | notifications | 只是告知,不回话 |
| 3 | 客户 -> 服务 | tools/list | 1 个工具的定义 |
| 4 | 客户 -> 服务 | tools/call | 结果文本 |

  四步之后连接就建好了。任何一个实现了这四步的宿主,都能用这个 Server。
  【换成 4 个宿主,也是这四步;换成 100 个,还是这四步。】
  子进程退出码 0,说明关得干净。

  ③ 把两个服务真调一遍
  --------------------------------------------------------------------

第一次调用,问天气:
  day    明天
  text   小雨
  low    17
  high   23

拿到 text 等于 小雨 之后,中间这一步判断——「下雨了,所以找室内的」——
在真机上是模型自己做的,这里换成一条 if。【判断这一步是替身,协议那部分不是。】

第二次调用,找地方:条件只剩下雨的室内景点,命中 2 条:
  浙江省博物馆           评分 4.6  门票 0 元
  中国丝绸博物馆          评分 4.5  门票 0 元

  ④ 差强人意的那个瞬间
  --------------------------------------------------------------------
用户接着说了下一句:帮我记一笔账,晚饭 56 块 5。
可眼下这个 Server 只会查天气和找地点,没有跟记账有关的工具。
  手上 2 个工具:weather.forecast、poi.search
  用户要的动作:记账   -> 没有工具能接
  只能回答:这件事我做不了
  这就是「不用 MCP」那种接法的天花板:【宿主接了什么,就只能做什么。】
  想加一个能力,得改宿主、重新发版、等用户升级。

换成 MCP,加能力不用动宿主,只要 Server 多挂一个工具:

Server 侧先往外推一条通知,说自己工具表变了:
    {
      "jsonrpc": "2.0",
      "method": "notifications/tools/list_changed"
    }
宿主收到之后重新列一次,从 2 个变成 3 个:
  weather.forecast   查某城市某天的天气
  poi.search         按城市和类型找地点
  note.add           记一条笔记

再问同一句话,这回有工具接了:
  {"ok": true, "total": 3, "text": "晚饭 56.5"}

  ⑤ 两种接法摆在一起
  --------------------------------------------------------------------

| 比什么 | 不用 MCP | 用 MCP |
|---|---|---|
| 要写的适配 | 宿主数乘服务数 = 24 份 | 服务数 = 6 个 Server |
| 加一个服务 | 每个宿主各加一份,4 处 | 加一个 Server,1 处 |
| 加一个宿主 | 每个服务各接一次,6 处 | 照协议连,0 处 |
| 接口改字段 | 24 份里挨个找 | 改 Server 一处 |
| 加一个能力 | 改宿主、发版、等升级 | Server 挂上新工具 |
| 换模型 | 适配全重写 | 一行不动 |

  ! MCP 没让「调接口」这件事变简单,它让「同一件事不用做第二遍」。

①第一段是「不用 MCP」的两份适配。同一个接口、同一个字段,只有返回值包装不一样。 看着像复制粘贴,这就是它的成本形态——不难,但会繁殖。 ②四行报文是本节的重点。它证明「连上」这件事跟写代码的技术栈无关,只跟说没说到同一句话有关。 子进程退出码是 0,说明收得干净。 ③中间那张表只有四行,值得抄下来。 以后任何一次「为什么我的工具在宿主里不显示」,都从这四行里找——多半是第 1 步或第 3 步。 ④第四步「做不了」是本节最像真实使用场景的一段。工具表里没有的能力,模型再聪明也变不出来; 反过来,工具表里多一个,宿主那边一行代码都不用动。 ⑤最后那张对照表里,只有第一行和第三行是真正省下的活。 中间几行是「改一处还是改一堆」,量小的时候看不出差别,量大以后是生死问题。

30 秒实验

  1. 把演示里的 4 个宿主改成 10 个,变量不变。算一下硬接要写多少份适配,MCP 要写几个 Server。
  2. 找一件你已经在用 AI 做、但每次都要自己贴数据的事。 判断它卡在「没有工具」还是「工具接错了地方」。
  3. 打开任意一个支持 MCP 的客户端,看一眼它列出来的工具表。数数有几个工具,描述写清楚了没有。

常见误区

  1. 「MCP 是个更快的接口调用框架。」它不碰网络、不碰接口,只规定「能力怎么自我介绍」。 真正发请求的还是你写的那几行。
  2. 「用了 MCP 就不用写代码了。」Server 里的查询逻辑一行不少,省掉的是为第二个宿主再写一遍。
  3. 「宿主升级一次就能支持新能力」这话不对。 宿主只负责照协议连,新能力挂在 Server 上,不挂在宿主上——这正是它比写死强的地方。
自测三问(点开对答案) 1. 4 个宿主、6 个服务,硬接要写多少份适配?走 MCP 要写几个 Server? —— 硬接 4 乘 6 等于 24 份;走 MCP 是 6 个 Server,宿主那边 0 处改动。 2. 「这件事我做不了」暴露了写死接法的什么问题? —— 能力清单在宿主里,加一个能力就得改宿主、发版、等升级。MCP 把清单挪到 Server 上,加了就生效。 3. 省下来的到底是哪部分代码? —— 不是查询逻辑,那本来就要写一次。省掉的是「为第二个宿主再写一遍」以及后续的同步修改。

案例看完了,下一个问题是这条约定从哪儿来。 下一节把三个日期、三次修订摆出来,再讲清楚一次连接里那句「我用哪一版」到底在说什么。

相关笔记