《MCP 协议设计与实现》完整目录
- 前言
- 第1章 为什么需要 MCP
- 第02章 架构总览:Host-Client-Server 模型
- 第03章 JSON-RPC 与消息格式
- 第04章 生命周期与能力协商(当前)
- 第05章 Tool:让 Agent 调用世界
- 第6章 Resource:结构化的上下文注入
- 第7章 Prompt:可复用的交互模板
- 第8章 TypeScript Server 实现剖析
- 第09章 TypeScript Client 实现剖析
- 第10章 Python Server 实现剖析
- 第11章 Python Client 实现剖析
- 第12章 STDIO 传输:本地进程通信
- 第13章 Streamable HTTP:远程流式传输
- 第14章 SSE 与 WebSocket
- 第15章 OAuth 2.1 认证框架
- 第16章 服务发现与客户端注册
- 第17章 sampling
- 第18章 Elicitation、Roots 与配置管理
- 第19章 Claude Code 的 MCP 客户端:12 万行的实战
- 第20章 从零构建一个生产级 MCP Server
- 第21章 设计模式与架构决策
第4章 生命周期与能力协商
在前面的章节中,我们了解了 MCP 的整体架构和 JSON-RPC 传输层。但一个根本性的问题还没有回答:当客户端第一次遇到服务端时,它们如何从"陌生人"变成"合作者"?这正是生命周期管理要解决的核心问题。
MCP 协议定义了一套严格的连接生命周期,确保客户端与服务端在开始正式通信之前,先就协议版本和功能边界达成共识。这不是形式上的握手礼仪,而是协议安全运行的基石------如果两端对"能做什么"没有共识,任何后续交互都可能导致不可预期的错误。
4.1 连接生命周期全景
一个 MCP 连接从建立到关闭,会经历三个明确的阶段:
第一阶段:初始化(Initialization) 。客户端发送 initialize 请求,携带自己支持的协议版本和能力声明;服务端回复自己的能力和选定的协议版本;客户端确认无误后发送 notifications/initialized 通知。这个阶段完成后,双方就建立了一份"合作契约"。
第二阶段:正常运行(Operation)。双方按照协商好的能力进行通信。客户端只能调用服务端声明支持的方法,服务端只能向客户端发起客户端声明支持的请求。
第三阶段:关闭(Shutdown)。通过传输层机制终止连接。对于 stdio 传输,客户端关闭输入流;对于 HTTP 传输,关闭相应的 HTTP 连接。
这三个阶段的划分不是建议性的,而是强制性的。规范明确要求:初始化阶段必须 是客户端与服务端之间的第一次交互。在初始化完成之前,客户端不应该 发送除 ping 以外的请求;服务端不应该 发送除 ping 和 logging 以外的请求。
4.2 初始化握手详解
初始化握手是整个生命周期中最关键的环节。让我们逐步拆解这个过程。
4.2.1 握手时序
整个过程遵循"三步走"的模式:请求 -> 响应 -> 通知。这个设计非常精妙------initialize 请求和响应完成了信息交换,而 notifications/initialized 通知则起到"确认信号"的作用,告诉服务端"我已经检查了你的响应,一切就绪,可以开始了"。
4.2.2 客户端发出的 initialize 请求
客户端发出的 initialize 请求包含三个核心字段:
json
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {
"roots": { "listChanged": true },
"sampling": {},
"elicitation": {}
},
"clientInfo": {
"name": "ExampleClient",
"version": "1.0.0"
}
}
}
protocolVersion:客户端期望使用的协议版本,通常是客户端支持的最新版本。capabilities:客户端声明自己支持的能力,如采样(sampling)、用户交互(elicitation)、文件系统根目录(roots)等。clientInfo:客户端的身份信息,包含名称、版本号,还可选地包含标题、描述、图标等展示信息。
4.2.3 服务端返回的 initialize 响应
服务端收到请求后,进行版本协商和能力声明:
json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-11-25",
"capabilities": {
"logging": {},
"prompts": { "listChanged": true },
"resources": { "subscribe": true, "listChanged": true },
"tools": { "listChanged": true }
},
"serverInfo": {
"name": "ExampleServer",
"version": "1.0.0"
},
"instructions": "本服务器提供文件管理和代码分析工具"
}
}
响应中有一个值得特别注意的字段:instructions。这是服务端给客户端(以及背后的 LLM)的自然语言指令,可以引导模型如何使用该服务端提供的工具和资源。
4.2.4 initialized 通知
客户端验证服务端响应后,发送一个简单的通知:
json
{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}
这个通知虽然简单,但意义重大------它标志着初始化阶段的结束和正常运行阶段的开始。服务端在收到此通知之前,不应该向客户端发送业务请求。
4.3 协议版本协商
版本协商是初始化握手中最精密的子流程。MCP 使用日期格式的版本号(如 2025-11-25),这种设计使版本的新旧关系一目了然。
4.3.1 协商算法
版本协商的规则如下:
- 客户端在
initialize请求中发送它支持的协议版本,应该是客户端支持的最新版本。 - 如果服务端支持该版本,必须回复相同的版本号。
- 如果服务端不支持该版本,必须 回复它自己支持的另一个版本号,应该是服务端支持的最新版本。
- 客户端收到响应后,检查服务端回复的版本是否在自己的支持列表中。如果不在,应该断开连接。
4.3.2 SDK 中的真实实现
让我们看看官方 SDK 中版本协商的真实代码。
Python SDK 的版本定义 (mcp/shared/version.py):
python
from mcp.types import LATEST_PROTOCOL_VERSION
SUPPORTED_PROTOCOL_VERSIONS: list[str] = [
"2024-11-05",
"2025-03-26",
"2025-06-18",
LATEST_PROTOCOL_VERSION # "2025-11-25"
]
TypeScript SDK 的版本定义 (@modelcontextprotocol/core 的 constants.ts):
typescript
export const LATEST_PROTOCOL_VERSION = '2025-11-25';
export const SUPPORTED_PROTOCOL_VERSIONS = [
LATEST_PROTOCOL_VERSION,
'2025-06-18',
'2025-03-26',
'2024-11-05',
'2024-10-07'
];
注意两个 SDK 的版本列表顺序不同------TypeScript SDK 把最新版本放在首位,Python SDK 把最新版本放在末尾。但这不影响协商逻辑,因为关键在于"是否包含",而非顺序。
TypeScript SDK 的客户端初始化代码(Client.connect):
typescript
// 发送 initialize 请求,使用支持列表中的第一个版本
const result = await this._requestWithSchema({
method: 'initialize',
params: {
protocolVersion:
this._supportedProtocolVersions[0] ?? LATEST_PROTOCOL_VERSION,
capabilities: this._capabilities,
clientInfo: this._clientInfo
}
}, InitializeResultSchema, options);
// 检查服务端回复的版本是否在支持列表中
if (!this._supportedProtocolVersions.includes(result.protocolVersion)) {
throw new Error(
`Server's protocol version is not supported: ${result.protocolVersion}`
);
}
// 保存协商结果
this._serverCapabilities = result.capabilities;
this._negotiatedProtocolVersion = result.protocolVersion;
服务端的对应逻辑(Server._oninitialize):
typescript
private async _oninitialize(
request: InitializeRequest
): Promise<InitializeResult> {
const requestedVersion = request.params.protocolVersion;
this._clientCapabilities = request.params.capabilities;
// 如果支持客户端请求的版本,就用它;否则用自己的首选版本
const protocolVersion =
this._supportedProtocolVersions.includes(requestedVersion)
? requestedVersion
: (this._supportedProtocolVersions[0] ?? LATEST_PROTOCOL_VERSION);
return {
protocolVersion,
capabilities: this.getCapabilities(),
serverInfo: this._serverInfo,
};
}
Python SDK 服务端的逻辑完全一致(ServerSession._received_request):
python
case types.InitializeRequest(params=params):
requested_version = params.protocol_version
await responder.respond(
types.InitializeResult(
protocol_version=requested_version
if requested_version in SUPPORTED_PROTOCOL_VERSIONS
else types.LATEST_PROTOCOL_VERSION,
capabilities=self._init_options.capabilities,
server_info=types.Implementation(
name=self._init_options.server_name,
version=self._init_options.server_version,
),
)
)
两个 SDK 的逻辑可以概括为同一个模式:服务端优先尊重客户端的版本偏好,只有在不支持时才降级到自己的最新版本。这种"客户端优先"的策略确保了向后兼容性------较新的服务端可以与较旧的客户端配合工作。
4.3.3 版本协商失败
当版本协商失败时,服务端可以返回一个详细的错误响应:
json
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32602,
"message": "Unsupported protocol version",
"data": {
"supported": ["2024-11-05"],
"requested": "1.0.0"
}
}
}
错误码 -32602 是 JSON-RPC 标准的"无效参数"错误码。data 字段中包含了服务端支持的版本列表和客户端请求的版本,便于诊断和调试。
4.4 能力声明机制
版本协商解决了"说同一种语言"的问题,而能力协商则解决了"能做哪些事"的问题。MCP 的能力声明机制是一种精巧的"特性开关"系统------双方在初始化时通过 capabilities 字段声明自己支持的功能,后续通信必须严格遵守这些声明。
4.4.1 为什么需要能力声明
考虑这样一个场景:客户端调用了 tools/list 方法,但服务端根本没有实现任何工具。如果没有能力声明机制,这个请求会导致不可预期的错误------可能返回一个空列表,可能抛出一个运行时异常,可能超时。而有了能力声明,客户端在初始化阶段就知道服务端不支持工具,因此根本不会发出这个请求。
能力声明的本质是一种契约前置------把运行时可能出现的不兼容问题,提前到连接建立阶段暴露出来。
4.4.2 服务端能力
服务端通过 capabilities 字段声明自己提供哪些功能:
| 能力 | 说明 | 子能力 |
|---|---|---|
tools |
提供可调用的工具 | listChanged:工具列表变更通知 |
resources |
提供可读取的资源 | listChanged:资源列表变更通知;subscribe:支持订阅单个资源的变更 |
prompts |
提供提示词模板 | listChanged:模板列表变更通知 |
logging |
发送结构化日志 | 无子能力 |
completions |
支持参数自动补全 | 无子能力 |
experimental |
实验性特性 | 自定义 |
listChanged 子能力 值得特别关注。当服务端声明 tools: { listChanged: true } 时,意味着它会在工具列表发生变化时主动通知客户端(发送 notifications/tools/list_changed)。客户端收到通知后可以重新获取工具列表,实现动态更新。这对于工具可能在运行时增减的服务端至关重要。
subscribe 子能力 是资源(resources)特有的。声明 resources: { subscribe: true } 的服务端,允许客户端订阅特定资源的变化。当资源内容更新时,服务端会通知已订阅的客户端。
4.4.3 客户端能力
客户端的能力声明告诉服务端:你可以向我发起哪些请求。
| 能力 | 说明 |
|---|---|
sampling |
支持 LLM 采样请求------服务端可以请求客户端调用 LLM 生成内容 |
roots |
提供文件系统根目录------服务端可以了解客户端的工作空间结构 |
elicitation |
支持用户交互请求------服务端可以请求客户端向用户展示表单或链接 |
experimental |
实验性特性 |
客户端能力中最重要的是 sampling 。这个能力赋予服务端一种独特的权力:向客户端请求 LLM 推理。这意味着一个不直接接入 LLM 的 MCP 服务端,可以通过客户端间接使用 AI 能力。但客户端如果没有声明 sampling 能力,服务端就不能发起 sampling/createMessage 请求。
roots 能力让服务端知道客户端管理着哪些文件系统根目录。子能力 listChanged 表示客户端会在根目录列表变化时通知服务端。
elicitation 能力允许服务端通过客户端与最终用户交互。服务端可以请求客户端展示表单或 URL,收集用户的输入后返回。
4.4.4 能力与方法的映射关系
这幅映射关系图揭示了 MCP 能力机制的核心设计理念:每个能力都精确对应一组方法。声明了某个能力,就等于承诺支持该能力下的所有方法。
4.5 enforceStrictCapabilities:严格模式
TypeScript SDK 提供了一个重要的选项:enforceStrictCapabilities。它控制在发出请求时,是否检查对端是否声明了支持相应的能力。
typescript
export type ProtocolOptions = {
enforceStrictCapabilities?: boolean;
// ...
};
当 enforceStrictCapabilities 设为 true 时,每次发出请求前都会调用 assertCapabilityForMethod,如果对端没有声明相应能力,请求会被立即拒绝,抛出 SdkError:
typescript
// Protocol._requestWithSchema 中的检查逻辑
if (this._options?.enforceStrictCapabilities === true) {
try {
this.assertCapabilityForMethod(request.method as RequestMethod);
} catch (error) {
earlyReject(error);
return;
}
}
客户端的能力检查逻辑清晰地映射了每个方法到对应的能力:
typescript
protected assertCapabilityForMethod(method: RequestMethod): void {
switch (method as ClientRequest['method']) {
case 'prompts/get':
case 'prompts/list':
if (!this._serverCapabilities?.prompts) {
throw new SdkError(
SdkErrorCode.CapabilityNotSupported,
`Server does not support prompts (required for ${method})`
);
}
break;
case 'resources/subscribe':
if (!this._serverCapabilities?.resources) {
throw new SdkError(/* ... */);
}
// 订阅还需要检查子能力
if (!this._serverCapabilities.resources.subscribe) {
throw new SdkError(
SdkErrorCode.CapabilityNotSupported,
`Server does not support resource subscriptions`
);
}
break;
case 'initialize':
case 'ping':
// 这两个方法不需要任何能力
break;
}
}
注意一个关键的设计决策:enforceStrictCapabilities 只检查远端的能力,不影响本地能力的检查。本地能力的错误声明被视为逻辑错误,始终会被检查。SDK 的注释明确解释了这一点:
Note that this DOES NOT affect checking of local side capabilities, as it is considered a logic error to mis-specify those.
当前 enforceStrictCapabilities 默认为 false,这是为了向后兼容早期没有正确声明能力的 SDK 版本。但 SDK 的文档已经提示:未来版本将默认设为 true。所以建议新项目从一开始就启用严格模式。
4.6 初始化失败与错误恢复
初始化不总是一帆风顺的。TypeScript SDK 的客户端在初始化失败时会自动断开连接:
typescript
override async connect(transport: Transport, options?: RequestOptions) {
await super.connect(transport);
try {
const result = await this._requestWithSchema(
{ method: 'initialize', params: { /* ... */ } },
InitializeResultSchema, options
);
// 版本检查、保存能力、发送 initialized...
} catch (error) {
// 初始化失败,关闭连接
void this.close();
throw error;
}
}
Python SDK 的客户端同样会在版本不匹配时抛出异常:
python
result = await self.send_request(
types.InitializeRequest(params=types.InitializeRequestParams(
protocol_version=types.LATEST_PROTOCOL_VERSION,
capabilities=types.ClientCapabilities(sampling=sampling, ...),
client_info=self._client_info,
)),
types.InitializeResult,
)
if result.protocol_version not in SUPPORTED_PROTOCOL_VERSIONS:
raise RuntimeError(
f"Unsupported protocol version from the server: "
f"{result.protocol_version}"
)
Python SDK 的服务端使用状态机来追踪初始化进度,拒绝在初始化完成前处理业务请求:
python
case types.InitializeRequest(params=params):
self._initialization_state = InitializationState.Initializing
# 处理初始化...
self._initialization_state = InitializationState.Initialized
case _:
if self._initialization_state != InitializationState.Initialized:
raise RuntimeError(
"Received request before initialization was complete"
)
这个状态机保证了一个不可绕过的约束:任何非初始化、非 ping 的请求,在初始化完成之前都会被拒绝。
4.7 优雅关闭
MCP 协议本身没有定义专门的关闭消息,而是复用传输层的关闭机制。这是一个务实的设计选择------不同传输方式有各自的关闭语义。
4.7.1 stdio 传输的关闭
对于 stdio 传输,规范定义了一个分级关闭流程:
- 客户端关闭子进程的输入流(即服务端的 stdin)
- 等待服务端自行退出
- 如果服务端在合理时间内没有退出,发送
SIGTERM - 如果
SIGTERM后仍未退出,发送SIGKILL
这种"优雅降级"的策略给了服务端完成清理工作的机会,同时保证了最终一定能关闭。
4.7.2 HTTP 传输的关闭
对于 HTTP 传输,关闭更加简单------关闭相应的 HTTP 连接即可。
4.7.3 SDK 中的关闭实现
TypeScript SDK 的 Protocol.close() 方法委托给传输层:
typescript
async close(): Promise<void> {
await this._transport?.close();
}
当连接关闭时(无论是主动关闭还是意外断开),_onclose 方法会执行全面的清理工作:
typescript
private _onclose(): void {
const responseHandlers = this._responseHandlers;
this._responseHandlers = new Map();
this._progressHandlers.clear();
this._transport = undefined;
// 通知所有等待响应的请求:连接已关闭
const error = new SdkError(
SdkErrorCode.ConnectionClosed, 'Connection closed'
);
for (const handler of responseHandlers.values()) {
handler(error);
}
// 取消所有正在处理的请求
for (const controller of requestHandlerAbortControllers.values()) {
controller.abort(error);
}
}
这段代码展示了一个负责任的关闭流程:不仅释放资源,还主动通知所有等待中的请求处理器,让它们能够妥善处理关闭事件,而不是悬在那里等待永远不会到来的响应。
4.8 超时与重连
4.8.1 请求超时
MCP 规范建议为所有请求设置超时。TypeScript SDK 默认的请求超时为 60 秒:
typescript
export const DEFAULT_REQUEST_TIMEOUT_MSEC = 60_000;
超时机制支持按请求自定义,还有一个精妙的设计------resetTimeoutOnProgress。当收到进度通知时,可以重置超时计时器。这对长时间运行的操作特别有用:只要服务端在持续汇报进度,客户端就知道它还在工作,不应该超时。同时,maxTotalTimeout 提供了一个绝对上限,防止因为持续的进度通知而无限等待。
typescript
export type RequestOptions = {
timeout?: number; // 单次超时
resetTimeoutOnProgress?: boolean; // 收到进度通知时是否重置超时
maxTotalTimeout?: number; // 最大总超时
// ...
};
4.8.2 重连机制
TypeScript SDK 的客户端支持重连。当传输层已经有 sessionId 时,connect 方法会跳过初始化握手,直接恢复之前协商好的协议版本:
typescript
override async connect(transport: Transport, options?: RequestOptions) {
await super.connect(transport);
// 如果已有 sessionId,说明是重连
if (transport.sessionId !== undefined) {
if (this._negotiatedProtocolVersion !== undefined
&& transport.setProtocolVersion) {
transport.setProtocolVersion(this._negotiatedProtocolVersion);
}
return; // 跳过初始化
}
// 否则执行正常的初始化握手...
}
这个设计使得 HTTP 传输在网络波动时能够快速恢复,而不需要重新走一遍完整的初始化流程。
4.9 设计洞察
回顾整个生命周期机制,我们可以提炼出几个核心设计原则。
契约前置原则。MCP 把所有兼容性问题都集中在初始化阶段暴露,而不是分散在后续的每次交互中。这大大简化了运行时的错误处理------一旦初始化成功,后续交互就可以在一个确定性更强的环境中进行。
能力驱动原则。MCP 的方法不是"所有人都可以调用"的,而是需要相应能力背书的。这种设计使得协议具有良好的可扩展性------新增功能时只需要新增能力声明,不需要修改已有方法的行为。
渐进协商原则。版本协商不是"全有或全无"的------当双方版本不完全一致时,通过协商找到一个双方都支持的版本。这保证了不同版本的实现可以互操作。
优雅降级原则。从初始化失败时的自动断开,到关闭时的分级信号(关闭流 -> SIGTERM -> SIGKILL),整个生命周期的异常处理都遵循"先尝试优雅,再强制执行"的策略。
这些设计原则不是 MCP 独创的------它们在 TLS 握手、HTTP 内容协商、WebSocket 建连等协议中都有体现。MCP 的贡献在于,它把这些经过验证的模式有机地组合在一起,构建了一个适合 AI Agent 场景的连接管理框架。
理解了生命周期和能力协商,我们就掌握了 MCP 运行时行为的"宪法"。后续章节讨论的工具调用、资源访问、采样请求等功能,都是在这个框架内运作的。在下一章中,我们将深入 MCP 的传输层,看看这些 JSON-RPC 消息是如何在不同的物理通道上传递的。