Model Context Protocol(MCP)是 Anthropic 在 2024 年底推出的开放协议,目标是给 LLM 应用和外部数据源、工具之间定一个标准化的接驳方式。它借用了 Language Server Protocol(LSP)的思路——LSP 让任意编辑器对接任意语言服务,MCP 则让任意 AI 应用对接任意工具与上下文。

这篇文章不写"怎么装一个 MCP Server"的入门教程,而是以规范为依据,逐层拆解 MCP 的协议设计:它的两层模型、JSON-RPC 消息格式、生命周期与能力协商,以及 server/client 双向的原语体系。理解了这些,你才能判断什么时候该用 MCP、什么时候不该用,以及自己实现一个 Server 时该注意什么。

为什么需要又一个协议

在 MCP 之前,给 LLM 接外部能力的做法基本是"每个应用各写一套"。Function calling 的 schema、工具发现的逻辑、鉴权方式、传输通道,全都耦合在具体应用里。结果是工具生态碎片化:你为 Cursor 写的工具不能给 Claude Desktop 用,为 OpenAI 写的 adapter 换到 Anthropic 又得重写。

MCP 想解决的就是这一层标准化。它不规定 LLM 怎么推理、怎么管理上下文,只规定"上下文和能力如何在 client 和 server 之间流动"。这是一个克制的边界——协议只管接驳,不管使用。也正因为边界清晰,它才能被不同厂商的 AI 应用共同采纳。

整体架构:三层参与者 + 两层协议

MCP 是 client-server 架构,但参与者有三类,容易混淆:

参与者角色例子
Host发起连接的 LLM 应用,负责协调多个 clientClaude Desktop、VS Code、Cursor
ClientHost 内部维护与单个 server 连接的组件Host 为每个 server 实例化一个 client
Server提供上下文与能力的程序,可本地可远程filesystem server、Sentry server

关键关系是 1:1 连接:一个 client 只对应一个 server。Host 想接 N 个 server,就开 N 个 client,每个 client 维护一条独立连接。这不是技术限制,而是隔离设计——一个 server 崩溃或变慢,不会拖垮其他 server 的连接。

1
2
3
4
5
6
7
8
9
┌─────────────── MCP Host (AI Application) ───────────────┐
│ │
│ Client 1 Client 2 Client 3 Client 4
│ │ │ │ │ │
└─────┼──────────────┼──────────────┼──────────────┼──────┘
│ │ │ │
▼ ▼ ▼ ▼
Server A Server B Server C Server C
(filesystem) (database) (Sentry,远程) (Sentry,远程)

注意 Server C 被两个 client 同时连——本地 server(stdio 传输)通常只服务一个 client,远程 server(Streamable HTTP 传输)则可服务多个 client。

协议本身分为两层,这是理解 MCP 的骨架:

  • Data Layer(数据层):基于 JSON-RPC 2.0 的消息协议,定义生命周期管理、能力协商和核心原语(tools、resources、prompts 等)。这是协议的"内核"。
  • Transport Layer(传输层):定义消息怎么在两端之间物理传输,负责连接建立、消息分帧、鉴权。这是协议的"外壳"。

传输层把通信细节抽象掉了,所以同一个 JSON-RPC 消息格式可以跑在任意传输机制上。这一点和 LSP 的设计如出一辙。

传输层:stdio 与 Streamable HTTP

MCP 规范目前定义两种传输机制,选择哪种基本取决于 server 跑在哪:

stdio 传输

client 把 server 当作子进程启动,通过标准输入/输出通信。每条 JSON-RPC 消息独占一行(或按规则分帧)。

  • 优点:零网络开销、零鉴权成本,本地进程间通信最快。
  • 限制:server 通常只服务一个 client;server 不能往 stderr 写协议数据(stderr 留给日志)。
  • 适用:filesystem、本地数据库、git 操作等"用户机器上的工具"。

Streamable HTTP 传输

远程 server 用 HTTP:client 通过 POST 发请求,server 可选地用 Server-Sent Events(SSE)做流式返回。这是 2025 年 3 月规范引入的,取代了早期的 HTTP+SSE 双通道方案——旧方案需要两条连接(一条 POST 上行、一条 SSE 下行),新方案在单条 HTTP 连接上完成,复杂度大幅下降。

  • 优点:可远程、可多 client 共享、可走标准 HTTP 鉴权(bearer token、API key、自定义 header)。
  • 规范推荐用 OAuth 获取 token。
  • 适用:Sentry、GitHub、企业内部 API 等"云上的能力"。

值得强调的一点:MCP 是有状态协议(stateful)。即便用了 Streamable HTTP,连接也维持会话状态,client 和 server 在整个生命周期内共享协商出来的能力集。规范里有一小部分可以做成无状态,但默认是有状态的——这直接影响下面的生命周期设计。

数据层:JSON-RPC 2.0 消息格式

MCP 的数据层就是 JSON-RPC 2.0,没有发明新轮子。三类消息:

1. Request(请求)——需要响应,带 id

1
2
3
4
5
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
}

2. Response(响应)——对请求的回复,id 对应,resulterror 二选一:

1
2
3
4
5
6
7
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [ /* ... */ ]
}
}

3. Notification(通知)——不需要响应,没有 id

1
2
3
4
{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}

判断一条消息是请求还是通知,看的就是有没有 id 字段——这是 JSON-RPC 的基本语义,但很多人实现时会忽略:notification 不该期待回复,发了就忘。

错误响应用标准的 error 对象,带 codemessage、可选 data。MCP 在 JSON-RPC 标准错误码之外,又定义了自己的一组(如 -32001 表示请求被取消),用于协议层语义。

生命周期:握手、运行、关闭

MCP 是有状态协议,所以连接必须经历明确的生命周期管理。核心目的是能力协商(capability negotiation):双方在握手时声明自己支持哪些特性,之后只发对方能理解的消息,避免运行时才发现"对方不支持"。

阶段一:Initialization(初始化握手)

client 主动发 initialize 请求,带上自己支持的协议版本和能力:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {
"elicitation": {}
},
"clientInfo": {
"name": "example-client",
"version": "1.0.0"
}
}
}

server 回应自己支持的版本和能力:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {
"tools": { "listChanged": true },
"resources": {}
},
"serverInfo": {
"name": "example-server",
"version": "1.0.0"
}
}
}

握手干了三件事:

  1. 协议版本协商protocolVersion(如 "2025-06-18")确保双方版本兼容。谈不拢就断开连接。
  2. 能力发现capabilities 对象声明各自支持哪些原语(tools、resources、prompts)以及附加特性(如 listChanged)。这是一份"我懂什么"的清单。
  3. 身份交换clientInfo / serverInfo 用于调试与兼容性判断。

注意例子里能力的方向:client 声明支持 elicitation(能接受 server 反向请求用户输入),server 声明支持 toolslistChanged: true(工具列表变了会主动通知)。能力是双向且独立声明的。

握手成功后,client 必须再发一条 notifications/initialized 通知,表示"我准备好了,进入运行态":

1
2
3
4
{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}

为什么用通知而不是再发一个请求?因为握手结果已经在 initialize 的响应里确定了,这一步只是个"开跑信号",不需要再等回复。

阶段二:Operation(运行)

进入运行态后,双方按协商好的能力正常收发消息:发现工具、调用工具、读资源、订阅通知等。能力协商的意义在这里兑现——client 知道这个 server 有 tools,所以会发 tools/list;知道那个 server 没有 resources,就不会白白发 resources/read

阶段三:Shutdown(关闭)

任一方可以发 shutdown 请求优雅关闭,或直接断开传输层。规范要求 server 在收到关闭信号后停止接收新请求、完成进行中的请求再退出。对 stdio 传输,client 关闭子进程的标准输入即可触发 server 退出。

整个生命周期的时序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Client                                          Server
│ │
│──── initialize (protocolVersion, caps) ───────▶│
│◀─── initialize result (protocolVersion, caps)─│
│ │
│──── notifications/initialized ────────────────▶│
│ │
│ ════════ Operation 阶段 ════════ │
│──── tools/list ───────────────────────────────▶│
│◀─── tools/list result ────────────────────────│
│──── tools/call ───────────────────────────────▶│
│◀─── tools/call result ────────────────────────│
│ │
│ ════════ Shutdown 阶段 ════════ │
│──── shutdown (或断开传输) ────────────────────▶│
│ │

核心原语:协议的真正主角

原语(primitive)是 MCP 最有意思的部分。它定义了"client 和 server 能互相给什么"。原语不是对称的——MCP 明确区分了由 server 暴露由 client 暴露两组,方向性是协议的核心设计。

Server 暴露的原语(server-controlled)

原语含义控制方典型方法
Tools可执行函数,让 AI 采取行动server 决定何时被调用tools/listtools/call
Resources数据源,提供上下文信息用户/应用决定何时读取resources/listresources/read
Prompts可复用的交互模板用户主动触发prompts/listprompts/get

三者的"由谁触发"是 MCP 一个微妙但重要的区分:

  • Tools 由模型决定:模型在推理过程中自主判断要不要调用某工具。这是 agentic 行为,对应 function calling。
  • Resources 由用户/应用决定:资源是给上下文用的数据(数据库 schema、文件内容、API 响应),读取由用户或应用控制,模型不该自己偷偷读。
  • Prompts 由用户触发:用户从 UI 里选一个预设模板(如"代码审查"工作流),是 slash-command 式的交互。

每个原语都遵循统一的三段式:发现(*/list)、获取/执行(*/gettools/call)、变更通知(*/list_changed)。这种规整的方法命名让 SDK 可以泛化处理。

Tool 的发现与调用是最典型的链路。先 tools/list 拿到工具清单:

1
2
3
4
5
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "weather_current",
"title": "Weather Information",
"description": "Get current weather information for any location",
"inputSchema": {
"type": "object",
"properties": {
"location": { "type": "string" },
"units": { "type": "string", "enum": ["metric", "imperial"] }
},
"required": ["location"]
}
}
]
}
}

inputSchema 是标准 JSON Schema,client 既能用它做参数校验,也能直接转成模型的 function calling schema。这就是 MCP 和 function calling 的衔接点——MCP 不取代 function calling,它替你把 function calling 的工具定义从"硬编码在应用里"变成"从 server 动态发现"

调用时:

1
2
3
4
5
6
7
8
9
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "weather_current",
"arguments": { "location": "San Francisco", "units": "imperial" }
}
}
1
2
3
4
5
6
7
8
9
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{ "type": "text", "text": "Current weather in San Francisco: 68°F..." }
]
}
}

返回值是 content 数组,每个元素带 type。支持 textimageresource 等多种类型,所以一个工具调用可以返回"文字+截图+一个资源引用"的组合体。这种多模态返回比传统 function calling 的单一字符串返回要灵活。

Client 暴露的原语(client-controlled)

这一组常被忽略,但它是 MCP 区别于"单向工具调用"的关键。Server 也能反过来请求 client 做事:

  • Sampling:server 请求 client 的 host LLM 生成一段补全。这让 server 作者能用到 LLM,又不必在 server 里塞某个具体模型的 SDK——保持模型无关。通过 sampling/createMessage 发起。典型场景是 server 实现"agentic 子任务":它自己编排多步推理,但把实际模型调用委托给 host。
  • Elicitation:server 反向请求用户补充信息或确认动作。通过 elicitation/create 发起。比如 server 要执行一个危险操作前,先问用户"确认删除吗?"。
  • Logging:server 把日志发给 client,用于调试和监控。

Sampling 的设计最能体现 MCP 的双向性。注意安全模型:协议故意限制 server 对 prompt 的可见性——用户必须显式批准 sampling 请求,控制是否发生、实际发什么 prompt、server 能看到哪些结果。这是规范里反复强调的"用户同意与控制"原则。

横切原语(utility)

除了 server/client 两组,还有跨切面的工具原语:

  • Notifications:实时更新。server 的工具列表变了,发 notifications/tools/list_changed,client 收到后重新拉 tools/list。这避免了轮询,让连接保持动态同步。
  • Progress tracking:长耗时操作报告进度。
  • Cancellation:取消进行中的请求。
  • Tasks(实验性):把一个请求包装成可延迟取回、可追踪状态的任务,用于昂贵计算、批处理、多步工作流。

通知:让协议"活"起来

通知是 MCP 动态特性的基础。一条 notifications/tools/list_changed 没有回复,server 发了就忘:

1
2
3
4
{
"jsonrpc": "2.0",
"method": "notifications/tools/list_changed"
}

但它受能力约束:只有 server 在初始化时声明了 tools.listChanged: true,才会发这种通知;client 也只有在协商时声明了相应能力,才能正确处理。能力协商和通知机制是配套的——先声明"我会通知你",再发通知,而不是突然发一个对方不知道怎么处理的消息。

这个模式让 MCP 连接能适应运行时变化:server 的工具可能因权限变化、外部依赖掉线而增减,client 不需要轮询,靠通知保持同步。Resources、Prompts 都有对应的 list_changed 通知。

一个完整的交互流程

把前面的零件拼起来,一次典型的 MCP 会话是这样:

  1. Host 启动,为配置的每个 server 创建一个 client。
  2. Client 与 server 完成 initialize 握手,记下 server 的能力(支持 tools,支持 listChanged)。
  3. Client 发 notifications/initialized,进入运行态。
  4. Client 调 tools/list 把所有 server 的工具汇总成一个统一注册表,注册给 LLM。
  5. 用户提问,LLM 决定调用 weather_current,Host 拦截这次 tool call,路由到对应 server 的 client。
  6. Client 发 tools/call,server 执行并返回 content,Host 把结果作为 tool result 喂回 LLM。
  7. 期间 server 若新增了工具,发 notifications/tools/list_changed,client 重新拉清单更新注册表。
  8. 会话结束,client 发 shutdown 或断开传输,server 退出。

整个链路里,MCP 真正定义的是第 2、3、4、6、7、8 步的协议格式。第 5 步"LLM 决定调用哪个工具"和"Host 怎么路由"——协议不管,那是应用层的事。这就是开头说的"协议只管接驳,不管使用"。

设计上的几点思考

拆完协议,几个值得回味的设计权衡:

为什么用 JSON-RPC 而不是 REST 或 gRPC。 JSON-RPC 2.0 是双向的:client 和 server 都能向对方发请求。REST 的方向是固定的(client 请求 server),无法表达 sampling、elicication 这种"server 反向请求 client"。gRPC 虽然支持双向流,但带 schema 编译、强类型绑定,对一个想被多语言生态快速采纳的协议来说太重。JSON-RPC 加 JSON Schema 刚好够用:双向、文本、易调试、几乎零工具链成本。

为什么能力协商放在握手阶段而不是运行时探测。 这是有状态协议的必然选择。连接维持整个会话,如果在运行时才"试一下对方支不支持",要么出错才知道、要么每个请求带一堆 feature flag。提前协商一次、把能力缓存下来,运行时直接按能力表发消息,干净且高效。代价是能力不能动态变化——所以才有 list_changed 通知做局部更新,而不必重新握手。

为什么 1:1 连接。 牺牲了复用,换来隔离。一个 server 进程崩溃、内存泄漏、响应变慢,最多影响连它的那个 client,其他 server 不受牵连。对于"接一堆来源不可信的第三方工具"的 Host 来说,这种隔离比共享连接池的吞吐更重要。远程 server 的多 client 共享是传输层 HTTP 自然带来的,和这个原则不冲突。

为什么 Tools / Resources / Prompts 要分三类而不是统一成"capability"。 因为它们的触发主体和信任模型不同:Tools 是模型自主调用(高权限,需用户授权每次执行),Resources 是用户/应用控制(中权限),Prompts 是用户显式触发(低权限,本质是模板)。混在一起就没法表达"这个能力该由谁发起、需要什么级别的同意"。分类的本质是权限分层。

结语

MCP 的价值不在于它有多复杂,恰恰在于它有多克制。它没有重新发明 RPC、没有自创消息格式、没有规定 LLM 怎么用上下文,只在"应用如何发现并调用外部能力"这一层做了标准化。JSON-RPC 2.0 + JSON Schema + 双向原语 + 能力协商,这套组合拳足以让任意 AI 应用和任意工具用同一种语言对话。

理解协议本身之后,再看那些琳琅满目的 MCP Server 实现,你会发现它们都只是同一套原语的不同填充:filesystem server 暴露 Resources(文件)+ Tools(读写),Sentry server 暴露 Tools(查 issue)+ Prompts(排障模板),数据库 server 暴露 Resources(schema)+ Tools(查询)。协议是骨架,生态是血肉。

如果你要自己实现一个 Server,记住几条:用 stdio 做本地、用 Streamable HTTP 做远程;握手时如实声明能力,别声明你做不到的;工具的 inputSchema 要严谨,那是 client 校验和转 function calling 的依据;返回多模态内容用 content 数组;长任务别让请求一直阻塞,用 progress 或 Tasks。把这些做对,你的 Server 就能被任何 MCP Host 调用——这正是标准化的意义。