MCP 是一个开放协议,通过 JSON-RPC 2.0 将 LLM 应用与外部工具和数据源解耦,类似 LSP 解决编辑器 N×M 集成问题,使 N×M 变成 N+M。
在 MCP 出现之前,每次集成都需要写两遍:一遍给工具,一遍给客户端。十个客户端各需要十个集成,就是一百个定制适配器,而且每个新客户端都得从零开始。这就是 N×M 问题,与 LSP 为编辑器和语言工具解决的正是同一类问题——这个类比并非偶然,MCP 正是公开地以 LSP 为蓝本设计的。
解决方案是在中间放置一个接口。为你的缺陷跟踪系统写一个服务器,任何符合规范的客户端都能使用它;写一个客户端,它就能使用所有服务器。N×M 变成 N+M。这就是全部的价值主张,也精确地告诉你这个协议何时值得它的开销:只有当 N 或 M 中任意一个确实大于一时。
消息格式采用 JSON-RPC 2.0——带 id 的请求、带 id 的响应,以及不带 id 且无回复的通知。没什么特别的,你甚至可以手动编写它,这在调试时很重要。
Host、client、server。Host 就是应用程序(一个 IDE、聊天应用或你的 Agent)。它为每个服务器创建一个客户端,每个客户端持有到该服务器的单一有状态连接。这种一对一配对是刻意设计的:正是它保证了服务器的能力和权限可以分离。
两种传输方式。stdio,即客户端将服务器作为子进程启动,通过其标准输入输出进行通信——这是本地工具的常见场景,没有端口问题也没有认证问题。另一种是 Streamable HTTP,用于远程服务器,单个端点处理 POST 请求并可升级为 server-sent events 进行流式传输。
按日期版本管理。协议版本是一个日期形式的字符串,在初始化时交换,客户端和服务器协商版本。版本不匹配是一个一等公民、可处理的条件,而不是谜之故障。
服务器可以提供三种东西,规范明确指定了谁应该在何时决定使用哪一种。这是设计中最有用的部分,也是大多数概述跳过的部分。
还存在从服务器流向客户端的特性:sampling/createMessage 让服务器请求宿主代表它运行 LLM 完成(所以服务器本身不需要 API key),roots 让客户端告诉服务器它可以在哪些文件系统或 URI 边界内操作,elicitation 让服务器在操作过程中请求用户补充输入。变更通知如 notifications/tools/list_changed 让服务器可以在运行时更改自己的目录,这是真正有用的,值得处理而不是忽略。
在任何有用的事情发生之前有三条消息,了解它们才能调试一个静默地什么都不做的连接:
client -> server
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
"protocolVersion":"2025-06-18",
"capabilities":{"roots":{"listChanged":true},"sampling":{}},
"clientInfo":{"name":"example-host","version":"1.4.0"}}}
server -> client
{"jsonrpc":"2.0","id":1,"result":{
"protocolVersion":"2025-06-18",
"capabilities":{"tools":{"listChanged":true},"resources":{}},
"serverInfo":{"name":"issues","version":"0.3.1"}}}
client -> server (a notification: no id, no reply)
{"jsonrpc":"2.0","method":"notifications/initialized"}
capabilities 对象就是协商过程。不声明 resources 的服务器会拒绝 resources/list,没有声明 sampling 的客户端不得被发送 sampling 请求。双方都被要求主动检查而非假想,这正是协议得以扩展而不破坏旧实现的原因。
初始化之后,客户端通常调用 tools/list,将每个条目的 inputSchema 映射到其模型提供商期望的工具格式,此后把模型的工具调用翻译为 tools/call。这个翻译层很薄——这是设计要点,也是 MCP 不替换 Agent 循环中那部分逻辑的原因。它替换的是工具的来源,而不是你如何处理它们。
安装第三方 MCP 服务器就是运行第三方代码,并赋予它对你所连接内容的访问权。除了这个显而易见的要点,还有三个是该协议特有的属性,值得内化:
工具描述是你模型上下文中的不可信文本。服务器提供自己的描述,它们被注入到 prompt 中。恶意描述可以包含指令。这是带安装步骤的提示注入,而且服务器在你安装时是可信的这一事实并不能约束它明天提供的内容——服务器可以随时更改其目录并用通知宣布。
人类审批是指定的要求,不是实现细节。规范的安全指导明确指出,宿主在调用工具之前以及在响应 sampling 请求之前应获取用户同意。如果你在写一个宿主,那是你的职责,而且它直接关系到审批门的位置。
远程服务器继承 Web 问题。HTTP 传输的服务器被要求验证 Origin header(DNS 重绑定可以从网页到达 localhost 服务器),在本地时应绑定到 localhost 而非所有接口,不应将客户端令牌传递给上游服务。绑定在 0.0.0.0 上且无 origin 检查的本地服务器可以被用户访问的任何页面触及。
MCP 买来的是互操作性,而互操作性是有代价的:进程边界、序列化跳跃、需要监督的子进程或 HTTP 依赖、需要调试的握手,以及你现在需要在某种格式而非类型系统中维护的 schema。当你有所收获时付出这个代价。不要在以下情况付出:
你是唯一的客户端。一个被你写的同一个 Agent 使用的工具从协议中毫无收获。装饰器函数更快、有类型、一个堆栈跟踪可调试、不需要传输层即可测试。
工具需要你应用程序的内部。如果它想要你的 ORM 会话、请求上下文或进程内缓存,边界位置就错了,你会花大量时间在边界上序列化对象。
延迟很紧。每次调用都是一次往返,而通过 stdio,是一个你必须保持存活和健康的子进程。对于微秒级函数这只是纯粹的 overhead。
目录很大。连接五个服务器各二十个工具,每个请求就有一百个 schema——多少工具算太多这个算术完全适用,而且 MCP 让添加工具变得非常容易,以至于会意外发生。
简单规则:在组织之间或应用程序之间的边界使用 MCP,在一个内部使用普通函数。如果你在为别人的 Agent 发布连接器,或消费别人的连接器,协议正在做它被设计来做的事。如果你在把你的数据库包装成子进程让你自己的 Agent 查询它,你加了一个跳转,得到的只是一个规范。