同一句「明天杭州下雨吗,下雨就找个室内的地方逛逛」, 接不接 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 秒实验
- 把演示里的 4 个宿主改成 10 个,变量不变。算一下硬接要写多少份适配,MCP 要写几个 Server。
- 找一件你已经在用 AI 做、但每次都要自己贴数据的事。 判断它卡在「没有工具」还是「工具接错了地方」。
- 打开任意一个支持 MCP 的客户端,看一眼它列出来的工具表。数数有几个工具,描述写清楚了没有。
常见误区
- 「MCP 是个更快的接口调用框架。」它不碰网络、不碰接口,只规定「能力怎么自我介绍」。 真正发请求的还是你写的那几行。
- 「用了 MCP 就不用写代码了。」Server 里的查询逻辑一行不少,省掉的是为第二个宿主再写一遍。
- 「宿主升级一次就能支持新能力」这话不对。 宿主只负责照协议连,新能力挂在 Server 上,不挂在宿主上——这正是它比写死强的地方。
自测三问(点开对答案)
1. 4 个宿主、6 个服务,硬接要写多少份适配?走 MCP 要写几个 Server? —— 硬接 4 乘 6 等于 24 份;走 MCP 是 6 个 Server,宿主那边 0 处改动。 2. 「这件事我做不了」暴露了写死接法的什么问题? —— 能力清单在宿主里,加一个能力就得改宿主、发版、等升级。MCP 把清单挪到 Server 上,加了就生效。 3. 省下来的到底是哪部分代码? —— 不是查询逻辑,那本来就要写一次。省掉的是「为第二个宿主再写一遍」以及后续的同步修改。案例看完了,下一个问题是这条约定从哪儿来。 下一节把三个日期、三次修订摆出来,再讲清楚一次连接里那句「我用哪一版」到底在说什么。
- ☐ (本篇完,下一篇 1.2 MCP 的历史、当下与未来)