Skip to content

第4章 生命周期与能力协商

在前面的章节中,我们了解了 MCP 的整体架构和 JSON-RPC 传输层。但一个根本性的问题还没有回答:当客户端第一次遇到服务端时,它们如何从"陌生人"变成"合作者"?这正是生命周期管理要解决的核心问题。

MCP 协议定义了一套严格的连接生命周期,确保客户端与服务端在开始正式通信之前,先就协议版本和功能边界达成共识。这不是形式上的握手礼仪,而是协议安全运行的基石——如果两端对"能做什么"没有共识,任何后续交互都可能导致不可预期的错误。

4.1 连接生命周期全景

一个 MCP 连接从建立到关闭,会经历三个明确的阶段:

第一阶段:初始化(Initialization)。客户端发送 initialize 请求,携带自己支持的协议版本和能力声明;服务端回复自己的能力和选定的协议版本;客户端确认无误后发送 notifications/initialized 通知。这个阶段完成后,双方就建立了一份"合作契约"。

第二阶段:正常运行(Operation)。双方按照协商好的能力进行通信。客户端只能调用服务端声明支持的方法,服务端只能向客户端发起客户端声明支持的请求。

第三阶段:关闭(Shutdown)。通过传输层机制终止连接。对于 stdio 传输,客户端关闭输入流;对于 HTTP 传输,关闭相应的 HTTP 连接。

这三个阶段的划分不是建议性的,而是强制性的。规范明确要求:初始化阶段必须是客户端与服务端之间的第一次交互。在初始化完成之前,客户端不应该发送除 ping 以外的请求;服务端不应该发送除 pinglogging 以外的请求。

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 协商算法

版本协商的规则如下:

  1. 客户端在 initialize 请求中发送它支持的协议版本,应该是客户端支持的最新版本。
  2. 如果服务端支持该版本,必须回复相同的版本号。
  3. 如果服务端不支持该版本,必须回复它自己支持的另一个版本号,应该是服务端支持的最新版本。
  4. 客户端收到响应后,检查服务端回复的版本是否在自己的支持列表中。如果不在,应该断开连接。

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/coreconstants.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 传输,规范定义了一个分级关闭流程:

  1. 客户端关闭子进程的输入流(即服务端的 stdin)
  2. 等待服务端自行退出
  3. 如果服务端在合理时间内没有退出,发送 SIGTERM
  4. 如果 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 消息是如何在不同的物理通道上传递的。

基于 VitePress 构建