得益于 Cursor 从 v0.45.x 开始支持 Anthropic MCP 协议,最近 MCP server 的概念很火热。我想聊聊对这个协议的感受。
MCP 是什么?
MCP = Model Context Protocol = 模型上下文协议
说白了,它就是个「插件协议」,严谨点加个限定词,「专供 LLM 应用的插件接口协议」。
Anthropic 官方说 MCP 是受微软的 LSP (Language Service Protocol) 的启发而制定,有朋友熟悉 LSP 协议的话,应该马上会发现这两者极为相似。
给不了解 LSP 的朋友介绍一下。VSCode 大家都熟,可以装各种插件。因为 VSCode 是用 JS 写的,插件要运行在 VSCode 之内,所以也必须用 JS 写。
但有一类插件比较特殊:编程语言支持类插件。比如你想在 VSCode 里写 rust,肯定要装 rust 相关插件。可问题是 rust 官方的语言支持(提供错误提示、代码自动补全之类的功能)肯定也是用 rust 写的,无法直接跑在 VSCode 的运行时里。别的语言 C#、Java、Python 情况也一样,怎么办呢?
为了解决这问题,LSP 制定了一套基于 JSON-RPC 2.0 的标准协议。RPC 顾名思义「远程调用」,那些语言工具你爱跑在哪都行,只要你按照这协议,能接受 RPC 请求,能给出正确返回数据格式,那么就能顺利接入 VSCode。
这套协议带来的价值有三个点:
-
这是个开放标准,市面上那么多 IDE 编辑器,都有语言支持需求,大家都用这套标准的话,很快可以形成开放插件生态。
-
把插件和消费它的客户端解耦合了。按照 LSP 标准写插件,你不需要关心你服务的客户端到底是 VSCode 还是 JetBrain 还是 Vim,只要这些客户端支持 LSP,那你的插件都能接入,不需要挨个适配。
-
LSP 协议本身预设了很多跟编程语言支持相关的「标准功能 」。例如最常见的代码自动补全
"textDocument/completion"
,或者点击跳转到函数定义"textDocument/definition"
等等。这些都是跨语言、广泛存在的需求,是编程语言业界多年积累下来的集体经验。假如你自己哪天创造了个新的编程语言,要写配套的语言支持工具,那么你不用闭门造车,对着 LSP 协议,把里列举的所有「标准功能」挨个实现一遍,这妥妥的就是「语言支持工具界的最佳实践」了。
所以 MCP 到底是什么...
之所以在 LSP 上费这么多字,是希望能借用一个大家熟悉的老概念,快速对 MCP 这个新概念建立起一个直观的认识。
回到 MCP,它也是一个基于 JSON-RPC 2.0 的标准协议,LSP 有的那些优点它也有:
-
开放标准:语言无关,实现无关,有助形成开放生态
-
解耦合:只要客户端支持,你的 MCP Server 都能接入,不用多次适配
-
最佳实践:参考「标准功能」,能借鉴行业集体经验,少走弯路
我认为「标准功能」,官方称为「能力(capability)」,是 MCP 价值比较大的东西,尤其对于开发 LLM 应用的朋友来说,支持这些能力基本上就跟 Cursor 在底层的 agent 工具层面上对齐了。
MCP 不是什么
MCP 不是 agent 框架,MCP 也不是 RAG 框架,它甚至都不是框架!尽管官方有提供 SDK,但 MCP 本身只是一个标准协议,目的是构建一个给 LLM/agent 用的「外接能力插件生态」。
不过 MCP 的标准设计里没有考虑 RAG 能力,是让我比较困惑的点。
能力 Capability
理解这个小节,我建议脑子里可以想着 Cursor 作为「LLM 应用」的范本。
client 端能力
-
roots
当前项目路径列表,对标 IDE 里的 workspace/project 概念,主要用来通知 server 端更新 resources(见下文) -
sampling
供 server 调用 client 侧 LLM 的能力
server 端能力
-
tools
任意的外部工具:计算器、代码运行、搜索引擎之类 -
prompts
提示词模版,设计目的是为了支持类似 Github Copilot Chat 聊天框/
开头的快捷指令
-
resources
当前项目下有什么资源可访问(主要是文件啦)。Cursor/Cline 聊天框@/foobar.txt
就可以用这项能力来实现 -
completion
自动补全,快捷指令和资源都需要,提升用户体验 -
logging
给到 client 的 log 信息推送,这个属于杂项,方便 debug 之类
其他
-
resources
不只能建模文件,也可以建模 git 历史,数据库表等其他资源,只需要 uri 上通过"git://"
或"db://"
来区分即可 -
两端都支持自定义能力,通过
experimental
namespace 来暴露。 -
前面提过 MCP 没考虑 RAG 的用例,目前看来似乎可以通过
prompts
+completion
能力来间接实现。
通讯模型
MCP 是一个 client/server 架构的 RPC 协议,需要关注两端的通讯模型。大致可分三段生命周期来看:初始化阶段,运行阶段,结束阶段。不过先铺垫两个前置知识,方便后面的理解。
前置知识
一、实体术语定义
一共有三类实体:host, client, server.
host 指「LLM 应用」的本体,它大概率是个 GUI 程序。在 MCP 的语境下,这就是一个容器,负责管理多个 client 实例,同时要集成 LLM,承接用户交互,特别是各种授权的工作。
每个 client 实例只负责与一个 server 建立有状态的连接,然后进行 RPC 通讯。server 是实际干活的、跑插件的线程,client 是留在 host 内负责 RPC 调用的一段简单的程序。
当前的协议版本下(版本号:2024-11-05
,你没看错,它是用日期来做版本号的)
-
host 与 client 是一对多关系
-
client 与 server 是一对一关系
这里插一句,「一对一」的奇怪设定是暂时的。目前 MCP 只针对 client/server 都跑在本地的场景设计,官方 SDK 在使用 stdio 为传输信道的时候,更是做了个「由 client fork 子进程来跑 server」的强假设。好在 roadmap 里面有提,支持 remote server 是眼下的第一优先级,预计 2025 上半年会更新相关标准。
二、JSON-RPC 2.0 的三种信息类型
JSON-RPC 2.0 标准有三类 RPC 信息类型:request, response, notification. 注意几个点:
-
request 必须有对应 response,
id
要对得上 -
response 的
result
和error
字段互斥,同时只可能有其一 -
response
error.code
必须是整数,并且协议预留了一批错误码,代表特定含义(类似 HTTP status code) -
notification 只比 request 少一个
id
, 并且不要求有对应 response
ts
type JsonRpcRequest = {
jsonrpc: "2.0";
id: string | number;
method: string;
params?: {
[key: string]: unknown;
};
};
type JsonRpcResponse = {
jsonrpc: "2.0";
id: string | number;
// result 与 error 是互斥的
result?: {
[key: string]: unknown;
};
error?: {
code: number;
message: string;
data?: unknown;
};
};
type JsonRpcNotification = {
jsonrpc: "2.0";
method: string;
params?: {
[key: string]: unknown;
};
};
三个通讯生命周期
一、初始化阶段 Initialization
client/server 需要握手协商,交换各自能力(capability)声明,跟 TCP 的三次握手基本一样。
第一次:client 向 server 发送 request,声明 client 侧提供的能力。
Client:「Server 老哥在吗?我能干这些,你能干啥?」
第二次:server 向 client 回复 response,声明 server 侧提供的能力。
Server:「Client 老弟,我在呢,我能干这些,需要干啥活你喊我哈!」
第三次:client 向 server 发送 notification,确认连接建立
Client:「得嘞,那我开始干活了,有事儿我再喊你。」
二、运行阶段 Operation
根据初始化阶段交换的能力声明,两端开始互相发送 RPC 信息。这里展示一段能力调用示例。
json
// 1. server 在初始化阶段,第二次握手时,向 client 公布自己的 tools 能力
{
"capabilities": {
"tools": {
"listChanged": true
}
}
}
// 2. client 初始化后,主动拉取 tools 列表
// Request:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {
// 可选参数,list 如果很长,可支持翻页
"cursor": "optional-cursor-value"
}
}
// Response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "get_weather",
"description": "Get current weather information for a location",
"inputSchema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City name or zip code"
}
},
"required": ["location"]
}
}
],
"nextCursor": "next-page-cursor"
}
}
// 3. client 调用工具
// Request:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": {
"location": "New York"
}
}
}
// Response:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "text",
"text": "Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy"
}
],
"isError": false
}
}
// 4. 如果 server 端因为什么原因,可用 tools list 发生变化,应该通知 client 重新拉取
{
"jsonrpc": "2.0",
"method": "notifications/tools/list_changed"
}
三、结束阶段 Shutdown
标准只说任何一端(正常来说是 client 端)可以主动断开连接,没有硬性规定这个阶段的具体协议。因为传输层通常会有相关的断联信号,已经够用了,没必要再在上层协议重复建设。
但是实际写落地实现,开发者还是需要做一些处理的,比如 graceful shutdown, 或者错误重启之类的。
总结
目前整个 AI 应用范式没有固定下来,整个业界都在积极探索,摸着石头过河。这个背景下 MCP 相当于把 AI 应用厂商们拉了个大群,一起来总结业界的最佳实践,制定标准推广集体智慧。当前 MCP 的生态发展势头很不错,标准本身更新得也很紧跟潮流。最近当红炸子鸡 Cursor 的加入,可以说是对 MCP 的重大利好,势必会进一步刺激 MCP server(插件)生态的成长。
现在正在做 LLM 相关应用的朋友,我非常推荐拥抱这个协议标准,好处多多。
-
首先协议本身很薄不复杂,看不出有技术上的坑。同时官方也有 SDK 可用,支持的难度不高。
-
其次可以拥抱生态,快速接入第三方插件,增强自身产品竞争力。
-
最后,让自己的应用去支持协议要求,等于是跟进业界最佳实践了,避免闭门造车走死胡同。
如果觉得本文对你有帮助,欢迎转发和关注(微信公众号同名),我会持续分享在开发 multi-agent 系统过程中的第一手经验和心得。