MCP 是怎样工作的?把模型上下文协议拆开看一遍
关于 MCP,流传最广的一句话是「AI 应用的 USB-C」。这个比喻好记,但它什么也没解释——USB-C 是个物理接口,MCP 是一套跑在 JSON-RPC 之上的应用层协议,两者唯一的共同点是「都想做统一接口」。比喻能让你记住它想干嘛,却挡住了你去看它到底怎么干的。
我平时用 Claude Code,顺手接过几个 MCP 服务器。接的过程中我发现,与其去记那些营销话术,不如把它当成一个普通网络协议来拆——就像我之前拆 Redis 的 RESP 一样:它在线上传的是什么字节、连接怎么建立、谁跟谁说话。拆完你会发现它出奇地朴素。
MCP(Model Context Protocol,模型上下文协议)是 Anthropic 在 2024 年 11 月开源的一套开放协议,规定 LLM 应用如何跟外部的工具和数据打交道。它不绑 Anthropic 自家模型,规范公开,任何人都能照着写客户端或服务器。
一、解决「M × N」问题
假设你有 M 个想接入大模型的应用(Claude、某个 IDE 插件、自己写的聊天机器人),和 N 个想被接进去的能力(GitHub、数据库、文件系统)。在没有统一约定的年代,每个应用要用上每个能力,都得单独写一套对接代码——M 个应用乘 N 个能力,就是 M × N 套各不相同的胶水代码。再加一个新工具,就得为现有的每个应用都补一遍。
MCP 做的事,就是在中间立一个标准。应用方实现一次「MCP 客户端」,工具方实现一次「MCP 服务器」,两边都只认这套协议,于是 M × N 塌缩成 M + N。新增一个工具,只要它说 MCP,所有支持 MCP 的应用就都能用上它,谁都不用再为谁单独改代码。
这就是它最实在的价值,也是「USB-C」那个比喻唯一想表达的点——只是它没告诉你中间那根「线」里跑的是什么。
二、三个角色:Host、Client、Server
MCP 里有三个角色,名字容易混,得先掰开:
- Host(宿主):你真正在用的那个应用,比如 Claude Code、Claude 桌面端、某个 IDE。它内置大模型,也负责管理下面的多个 Client。
- Client(客户端):活在 Host 内部的连接器。一个 Client 只跟一个 Server 一一对应,是 1:1 的关系。Host 想连三个服务器,就在内部开三个 Client。
- Server(服务器):一个独立的程序,对外暴露具体能力——读文件、查数据库、调 GitHub API。它可以是你机器上的一个子进程,也可以是一个远程服务。
这里有个容易绕的点:模型本身不直接跟 Server 说话。是 Host 里的 Client 跟 Server 通信,把结果整理好再喂给模型。Server 也从不直接碰模型,它只管暴露能力、响应请求。把这层关系理顺,后面的消息流向就不会乱。
「一个 Client 对一个 Server」这条规矩也值得记一下——它意味着每条连接都是独立的、隔离的。一个服务器崩了或者行为异常,影响的是它那条连接,不会污染其它的。
三、JSON-RPC 2.0
把架构掀开,MCP 在线上传的每一条消息,都是一个 JSON-RPC 2.0 报文。没有自定义的二进制格式,就是普通的 JSON。所以它一共只有三种消息:
请求(Request)——带 id,期待对方回一条对应的响应:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": { "name": "create_issue", "arguments": { "...": "..." } }
}
响应(Response)——id 跟请求对得上,要么有 result,要么有 error,二选一:
{
"jsonrpc": "2.0",
"id": 3,
"error": { "code": -32602, "message": "缺少必填参数 repo" }
}
通知(Notification)——没有 id,发完就完,对方不回:
{ "jsonrpc": "2.0", "method": "notifications/tools/list_changed" }
就这三种。id 用来把请求和响应配对——因为可以同时有好几个请求在飞,靠 id 才知道哪条响应对应哪条请求。method 是方法名,MCP 把它按 分类/动作 来命名(tools/list、resources/read、prompts/get),看名字就知道在操作哪类东西。
通知专门用来传那种「不需要确认的事件」。最典型的就是上面那条 notifications/tools/list_changed——服务器的工具列表变了,发个通知告诉客户端「你手上的清单过期了,重新拉一遍」,仅此而已,不需要回话。
理解到这一层,MCP 就没什么神秘的了:它就是一套约定好了方法名和参数结构的 JSON-RPC。剩下要搞清的,无非是「有哪些方法」和「连接怎么建立」。
四、连接是怎么建立的:握手与能力协商
连接建立的第一件事,是一次初始化握手。这步很关键,因为客户端和服务器谁支持什么、版本对不对得上,全在这里谈妥。
流程是三步:
- 客户端发一条
initialize请求,亮出自己支持的协议版本和能力(capabilities),外加自己的名字版本号。 - 服务器回一条响应,同样亮出它的协议版本和它支持的能力。
- 客户端再发一条
notifications/initialized通知,握手完成,进入正常运行。
initialize 请求长这样:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {
"roots": { "listChanged": true },
"sampling": {}
},
"clientInfo": { "name": "claude-code", "version": "1.0.0" }
}
}
服务器的响应:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {
"tools": { "listChanged": true },
"resources": { "subscribe": true, "listChanged": true },
"prompts": {}
},
"serverInfo": { "name": "my-server", "version": "0.3.1" }
}
}
这里有两个点值得留意。
一个是协议版本是个日期串,像 2025-06-18,不是 1.0 这种语义版本号。MCP 用发布日期当版本号(先后出过 2024-11-05、2025-03-26、2025-06-18 几版)。两边在握手时确认能不能在同一版本上对话,对不上就在这一步谈不拢,连接根本不会建立——这点比很多协议「先连上再说,出错再处理」要干脆。
另一个是能力协商(capability negotiation)。双方在握手时各自声明「我支持哪些功能」,之后就只在交集里活动。服务器说自己有 tools,客户端才会去调工具相关的方法;服务器没声明 prompts,客户端就不会去问提示词。那些 listChanged、subscribe 是更细的开关——比如 resources.subscribe: true 表示「我这边的资源支持订阅变更」。能力是协商出来的,不是假定出来的,这让不同实现之间能优雅地降级,而不是直接报错崩掉。
五、Server 暴露的三类原语:Tools、Resources、Prompts
握完手,正题来了:服务器到底能暴露什么?规范定义了三类原语(primitives)。很多教程会把它们并列成「三个功能」,但我觉得那样讲会漏掉最要紧的一点——它们真正的区别,是「由谁来决定用它」。
Tools(工具)——由模型决定调用。 这是让模型去「做事」的函数:建一个 issue、跑一条查询、发一封邮件。服务器用 tools/list 把工具清单连同每个工具的描述和参数 schema 交出去,模型读懂描述后,自己决定什么时候、带什么参数去调,调用走 tools/call。主动权在模型手上。
工具列表大致是这样(核心是那个 inputSchema,它用 JSON Schema 描述参数,模型靠它知道该怎么填):
{
"name": "create_issue",
"description": "在指定仓库创建一个 issue",
"inputSchema": {
"type": "object",
"properties": {
"repo": { "type": "string" },
"title": { "type": "string" },
"body": { "type": "string" }
},
"required": ["repo", "title"]
}
}
模型决定调用时,发 tools/call:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "create_issue",
"arguments": {
"repo": "meiki/blog",
"title": "图片懒加载没生效",
"body": "首屏以下的图没加 loading=lazy"
}
}
}
服务器执行完,把结果放在 content 里回来(content 是个数组,因为结果可以是文本、图片等多种块):
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{ "type": "text", "text": "已创建 issue #42" }
],
"isError": false
}
}
Resources(资源)——由应用决定注入。 这是供模型参考的只读数据:一份文件、一条数据库记录、一段日志。每个资源用一个 URI 标识,resources/list 列出来,resources/read 读内容。关键区别在于:不是模型自己去抓,而是宿主应用决定把哪些资源塞进上下文。打个比方,工具是「模型能按的按钮」,资源是「应用端摆出来给模型看的资料」。如果服务器声明了 subscribe,客户端还能订阅某个资源,等它变了收到通知。
Prompts(提示词)——由用户主动触发。 这是服务器预先准备好的模板或工作流,由用户显式选用。落到产品上,最常见的形态就是斜杠命令——你在 Claude Code 里敲 / 弹出来的那些,很多就是某个 MCP 服务器通过 prompts/list 提供、用 prompts/get 取出来的。主动权在用户。
把这三者的控制方串起来记最清楚:
Tools 模型说了算,Resources 应用说了算,Prompts 用户说了算。
这条线我觉得是理解 MCP 设计的钥匙。它不是把所有外部能力一股脑塞给模型任意调用,而是按「该由谁做主」做了切分——该模型自主的归 Tools,该应用控场的归 Resources,该用户拍板的归 Prompts。这种克制,比能力本身更能说明它想成为一个长期标准的意图。
顺带一提,原语不只服务器有,客户端也能反过来给服务器提供能力,常见三个:sampling(服务器反过来请客户端这边的模型生成一段内容,这样服务器不用自带 API key)、roots(客户端告诉服务器它能碰的文件系统边界在哪)、以及较新版本加入的 elicitation(服务器中途向用户要一条补充信息)。这几个用得没前三类多,但它解释了为什么握手时客户端也要声明自己的 capabilities——通信是双向的。
六、两种传输:stdio 与 Streamable HTTP
最后一块拼图是传输层。同样一套 JSON-RPC 消息,可以走两种通道,取决于服务器在哪儿。
stdio——跑在本地。 服务器是 Host 直接拉起的一个子进程,双方就通过标准输入输出(stdin / stdout)通信,每条 JSON-RPC 消息占一行、用换行分隔。没有网络、没有鉴权,启动即用。本地文件、本地数据库、命令行工具这类「能力就在你自己机器上」的场景,几乎都走 stdio。我接的几个本地服务器全是这种,配起来基本就是在配置里写一条「用什么命令把它拉起来」。
Streamable HTTP——跑在远程。 服务器是个远程 HTTP 服务,客户端用 POST 把请求发到一个端点(比如 /mcp),需要流式返回时服务器用 SSE(Server-Sent Events)持续往回吐数据。这适合那种由服务方托管、多人共用的远程能力。
这里有个历史包袱值得说一句:早期规范用的是「HTTP + SSE」的双端点方案,从 2025-03-26 那一版起换成了现在的单端点 Streamable HTTP。如果你翻到比较老的教程,看到的可能是旧方案,对不上是正常的——这也侧面说明这协议还在快速变。
七、碎碎念
它确实朴素,但「朴素」不等于「成熟」。 协议本身干净,可生态还很新。我接服务器时踩到的麻烦,基本不在协议层,而在「这个第三方服务器质量怎么样、文档全不全、版本跟没跟上」。规范定得好,不保证每个实现都靠谱。
鉴权是最该当心的地方。 stdio 在本地、问题不大;但远程 HTTP 服务器意味着你在授权一个外部服务去碰你的数据和工具。基于 OAuth 2.1 的鉴权框架是后来的版本才补齐的,属于这套协议里相对年轻、也最需要你自己看清楚再点同意的部分。接一个来路不明的远程 MCP 服务器,跟随便给一个第三方应用授权没有本质区别——别因为它叫「MCP」就放松。
工具描述的质量,直接决定模型用不用得好。 因为 Tools 是模型自主调用的,那段 description 和 inputSchema 写得清不清楚,几乎等同于工具好不好用。我见过描述含糊的工具,模型要么不调、要么调错参数。这部分是服务器作者的功夫,不是协议能替你兜的。
工具一多,是有成本的。 每个连上的服务器都会把自己的工具清单注入模型的上下文。接十个服务器、几十个工具,光这份清单就占掉可观的 token,还可能让模型在「该选哪个工具」上犯迷糊。我的做法是只接当下真用得上的,不图全。
它还在快速演进。 前面那几个日期版本号、换掉的传输方案、后补的鉴权和 elicitation,都说明规范远没定型。现在写的对接代码,过段时间可能就得跟着改。把它当一个「方向明确但仍在路上」的标准看,比当成稳定地基靠谱。
小结
MCP 没有它的名号听上去那么玄。把它拆开,就是一套跑在 JSON-RPC 2.0 之上、约定好了方法名和握手流程的应用层协议:用 M + N 替掉 M × N 的对接,靠 Host / Client / Server 三层分工,靠初始化握手协商能力,再用 Tools / Resources / Prompts 三类原语按「该由谁做主」切分外部能力——模型、应用、用户各管一摊。
想看一手规范、所有方法的完整定义和最新版本,去官方文档:Model Context Protocol 规范。等我接的服务器再多些、踩过更多真实的坑,再回来更新这篇。