读懂 MCP 之后,我才发现自己混淆了什么
前几天,我读完一篇介绍 MCP 的文章,第一反应是:这个概念并不难。
如果要用一句话概括,我会说它像 AI 应用世界里的 USB 接口。只要工具和应用都遵循同一套协议,接入新工具、切换模型就会方便很多。
这句话听起来顺畅,也没有完全错。但当我真的关掉文章,尝试画出一次完整调用链时,问题很快暴露出来了。
我把 MCP 和 Function Calling 当成了两套互相替代的方案;我以为 MCP Client 会判断要不要调用工具;我还认为用了 MCP 以后,切换模型大概率只需要修改配置。
这些理解都抓到了一点表象,却没有分清系统边界。
这次经历正好接上了我之前总结的“断电测试”:读文章时觉得懂了,不代表离开作者的表达后,还能独立重建其中的角色、因果和边界。
MCP 和 Function Calling 不是替代关系
我现在会把两者拆成两个问题来看。
Function Calling 解决的是模型如何向应用表达调用意图。 模型根据用户问题和工具定义,输出结构化的工具名称与参数,例如 tool_calls。它并不负责真正执行函数。
MCP 解决的是应用如何用统一协议连接外部能力。 它规范 MCP Client 与 MCP Server 之间的初始化、能力发现、工具调用、结果返回和传输方式。
中间负责协调这一切的是 Host。Host通常会:
- 管理一个或多个 MCP Client;
- 从 MCP Server 获取工具定义;
- 把工具定义转换成当前模型能够使用的格式;
- 接收模型产生的工具调用;
- 将调用路由到对应的 MCP Server;
- 把工具结果重新交给模型生成答案。
所以,MCP 和 Function Calling 完全可以出现在同一条链路中。
模型负责选择,Host负责协调,Client负责协议通信,Server负责暴露并执行具体能力。把这四件事混在一起,后面的理解就会不断出错。
工具发现必须发生在模型选择之前
我第一次画 MCP 调用链时,把 tools/list 放在了模型决定调用工具之后。
这在因果上说不通。模型如果事先不知道有哪些工具、工具叫什么、参数Schema是什么,就没有依据产生可靠的工具调用。
更合理的顺序是:
Host建立MCP连接
-> Client完成初始化与能力协商
-> Client通过tools/list发现工具
-> Host注册并整理工具定义
-> Host把用户问题和工具定义交给模型
-> 模型产生工具调用
-> Host通过对应Client发送tools/call
-> Server执行工具并返回结果
-> Host把结果交回模型
-> 模型生成最终答案
这里的“发现工具”通常不需要在每次用户提问时重新进行。Host可以在启动或连接建立阶段维护工具注册表,并在工具列表变化时更新。
MCP 连接为什么不能上来就调用工具
MCP是有状态协议。Client与Server开始正常通信前,需要先完成初始化。
它的基本时序是:
Client发送initialize请求
-> Server返回initialize结果
-> Client发送notifications/initialized
-> 双方进入Operation阶段
initialize请求至少会携带Client支持的协议版本、Client能力和实现信息;Server响应协商后的协议版本、Server能力和实现信息。
这里有两个容易混在一起的概念:
- 版本协商回答“双方按照哪个协议版本通信”;
- 能力协商回答“这次会话允许使用哪些功能”。
如果Server最终返回的版本不在Client支持范围内,Client应该断开连接。如果Server没有声明 tools 能力,Client就不应该调用 tools/list 或 tools/call。这不是权限意义上的“越权”,而是对应能力没有被协商出来。
notifications/initialized也不是用来证明底层连接一定成功。它表达的是:Client已经处理完初始化结果,现在准备进入正常操作阶段。
MCP 标准化了什么,又没有解决什么
MCP协议主要覆盖两个层次。
数据层基于 JSON-RPC 2.0 定义消息结构和语义,包括生命周期、能力协商、Tools、Resources、Prompts和通知等机制。
传输层负责Client与Server之间的连接建立、消息承载和分帧等通信问题。常见方式包括本地 stdio 和远程 Streamable HTTP。
但MCP并没有直接解决下面这些问题:
- 模型应该选择哪个工具;
- 工具内部的业务逻辑是否可靠;
- 多个工具应该按什么顺序编排;
- 工具结果是否真实、完整和安全;
- 不同模型厂商的Function Calling接口差异;
- Agent什么时候重试、终止或请求人工确认。
因此,“用了MCP以后切换模型只需要改配置”并不是协议保证。
如果Host已经把不同模型的API、工具Schema和返回格式抽象好了,切换模型确实可能只表现为修改配置。但换模型后,工具选择准确率、参数生成、指令遵循和异常行为仍然需要重新验证。
从协议概念走向工具契约
只会画架构图仍然不等于会实现。我为自己设计了一个最小的 learning_notes MCP Server:
search_notes:在允许目录中搜索Markdown笔记;read_note:按偏移和行数限制读取指定笔记;learning://rules:向Client提供允许目录、文件类型和读取上限。
设计过程中,真正费脑子的不是给函数加一个MCP装饰器,而是把契约说完整。
例如,read_note既然支持分段读取,输入中就必须明确 offset 和 limit,输出里的同名字段也必须保持相同语义。max_results超过上限时,是静默截断还是返回参数错误,也必须确定唯一行为,不能让调用方猜。
文件读取还会带来路径穿越和符号链接问题。只检查输入中有没有 .. 并不够,应该先得到真实规范化路径,再判断它是否仍在允许根目录下。
from pathlib import Path
def resolve_safe_path(root: Path, user_path: str) -> Path:
allowed_root = root.resolve()
target = (allowed_root / user_path).resolve()
try:
target.relative_to(allowed_root)
except ValueError as exc:
raise ValueError("path is outside the allowed root") from exc
if target.suffix.lower() != ".md":
raise ValueError("only Markdown files are allowed")
return target
即使是一个只读工具,也至少要继续考虑:文件不存在、输入类型错误、结果过长、编码失败、符号链接越界和错误信息泄漏。
这让我意识到,MCP带来的价值不是替代工程设计。它只是让工具以统一方式被发现和调用,工具本身的输入校验、安全边界和失败处理仍然需要开发者负责。
我现在到底掌握到了哪一步
如果按“看过、能解释、能实现、已验证、可面试”来划分,我现在完成的是“能解释”以及一份实现前的契约草稿。
我能够说明:
- MCP与Function Calling的关系;
- Host、Client、Server的基本职责;
- 工具发现和调用的完整顺序;
- 初始化、版本协商和能力协商;
- MCP负责与不负责的边界;
- 一个只读工具在实现前需要考虑的契约和路径安全。
我还没有完成Python MCP Server、真实Client调用、自动化测试和项目接入。因此,目前不能把“熟悉MCP应用集成”写进简历,也不能把这份设计稿当成项目经验。
后续如果在Agent项目中真正使用MCP,我希望至少留下这些证据:真实工具、调用日志、正常与失败测试、超时和安全边界,以及一次脱离原实现的独立修改。
留给下一次断电测试的问题
过几天重新阅读这篇文章时,我不准备先看正文,而是先回答下面几个问题:
- MCP与Function Calling分别解决什么问题?
- 为什么
tools/list必须发生在模型选择工具之前? - Host、Client、Server各自负责什么?
initialize、初始化响应和notifications/initialized的顺序是什么?- 协议版本协商与能力协商有什么区别?
- 为什么路径安全不能只检查
..? - 接入MCP后,切换模型为什么仍然需要回归验证?
如果这些问题只能靠重新翻文章才能回答出来,那我拥有的仍然只是熟悉感,而不是可以使用的理解。
这大概也是这次学习最有价值的部分:我没有因为记住了几个术语就急着宣布“学会MCP”,而是逐步发现自己在哪些边界上理解错了,并把模糊的认识改造成了可以继续验证的结构。
参考资料
中文翻译版本比官方当前规范旧,本文中的核心流程已按官方2025-11-25版本复核。涉及新增能力、授权和具体实现时,仍应以官方最新规范为准。
更多推荐

所有评论(0)