9.2 连接生命周期:async with 与协议协商 本节摘要:本节讲 的连接生命周期—— 进入时发生了什么、协议版本怎么协商。核心是「构造期与连接期分离」:构造只选传输, 才真正打开连接、协商协议版本、交换能力声明。我们会讲透 参数( / /特定版本)如何控制协议协商,以及现代连接(2026-07-28)为何无需握手。读完本节,你能精确控制客户端如何与服务端「互相了解」。 一、async with 里发生的事 回顾第 9.1 节:构造期只选传输,连接在 时打开。具体地, 进入时,SDK 依次做这些: 退出 时,SDK 关闭连接、清理资源。这就是「连接生命周期」的完整过程,全部由 自动管理。 二、协议版本协商 第 2 步「协议版本协商」值得详讲。
本节摘要:本节讲
Client的连接生命周期——async with进入时发生了什么、协议版本怎么协商。核心是「构造期与连接期分离」:构造只选传输,async with才真正打开连接、协商协议版本、交换能力声明。我们会讲透mode参数(auto/legacy/特定版本)如何控制协议协商,以及现代连接(2026-07-28)为何无需握手。读完本节,你能精确控制客户端如何与服务端「互相了解」。
回顾第 9.1 节:构造期只选传输,连接在 async with 时打开。具体地,async with Client(...) as client: 进入时,SDK 依次做这些:
async with Client(url) as client: │ ▼ 进入上下文,SDK 自动执行: 1. 打开传输连接(发 HTTP / 起 stdio / 建内存分发) │ ▼ 2. 协议版本协商(按 mode 参数) - 确定双方都支持的协议版本 │ ▼ 3. 交换能力声明 - 客户端告知自己的能力(支持采样、根等) - 服务端告知自己的能力(tools/resources/prompts 等) │ ▼ 4. 连接就绪,client 对象可用 - 可以调 call_tool、read_resource 等
退出 async with 时,SDK 关闭连接、清理资源。这就是「连接生命周期」的完整过程,全部由 async with 自动管理。
第 2 步「协议版本协商」值得详讲。MCP 协议有版本(对应规范修订,如 2026-07-28),客户端与服务端要协商一个双方都支持的版本:
客户端支持的版本:[2026-07-28, 2025-11-25, ...] 服务端支持的版本:[2026-07-28, 2025-11-25, ...] │ ▼ 协商 双方都支持的最高版本:2026-07-28 │ ▼ 用这个版本通信
协商确保「新客户端连老服务端」「老客户端连新服务端」都能工作——双方各自降级到都支持的最高版本。
Client 的 mode 参数控制协议协商行为:
async with Client(url, mode="auto") as client: # 默认:auto ...
mode 的几种取值:
| mode | 行为 | 适用场景 |
|---|---|---|
"auto"(默认) |
自动协商,选双方都支持的最高版本 | 绝大多数场景 |
"legacy" |
强制走旧版 initialize 握手 | 连老服务端、需要 legacy 特性 |
特定版本字符串(如 "2026-07-28") |
强制用指定版本 | 测试、强制现代/legacy |
# 默认自动协商 async with Client(url) as client: ... # 强制 legacy 模式 async with Client(url, mode="legacy") as client: ... # 强制特定版本 async with Client(url, mode="2026-07-28") as client: ...
mode 的核心差异在「握手」——现代协议(2026-07-28)与 legacy 协议的连接建立方式不同:
现代连接(2026-07-28): 客户端发请求(带期望版本) 服务端直接响应(确认版本 + 能力) → 无独立握手,首个请求即建立 → 简单、低延迟 legacy 连接(2025 及更早): 客户端发 initialize 请求 服务端响应 initialize(能力声明) 客户端发 initialized 通知 → 三步握手,才进入正常通信 → 复杂、多往返
现代协议移除了 initialize 握手,让连接建立更简单、更省往返。这是 2026-07-28 协议简化的体现(与第 7.3 节「移除服务端发起请求」一脉相承)。
💡 技巧:默认
mode="auto"就好。它会自动选最佳版本——服务端支持现代就连现代(无握手、低延迟),不支持就降级 legacy。只有当你明确需要强制某个版本(如测试、兼容老服务端)时,才显式指定 mode。
第 3 步「能力声明交换」是双向的:
客户端告知服务端: "我支持 sampling(采样)、roots(根)、elicitation(引导填写)" → 服务端据此知道:可以反向请求这些能力 服务端告知客户端: "我支持 tools、resources、prompts" → 客户端据此知道:可以问 tools/list、resources/read 等
这个双向声明让双方知道「对方能做什么」,避免发出对方无法响应的请求。回顾第 3.3 节,服务端的能力声明是从注册的处理器推断的;客户端的能力声明则基于它实现了哪些反向能力。
关键规则:如果客户端没声明支持某个能力(如 sampling),服务端就不该尝试用那个能力。这就是为什么第 7 章「采样弃用趋势」——越来越少客户端声明支持它。
async with 进入时如果连接失败(网络问题、版本不兼容、认证失败),会抛异常。常见的:
| 异常 | 原因 | 处理 |
|---|---|---|
| 连接超时 | 网络问题、服务端没起 | 检查 URL、服务端状态 |
| 协议版本不兼容 | 客户端与服务端无共同版本 | 升级 SDK、检查 mode |
| 认证失败 | OAuth token 无效 | 第 11 章,重新认证 |
| 能力不匹配 | 客户端要求的能力服务端没有 | 检查 server_capabilities |
try: async with Client(url) as client: result = await client.call_tool(...) except ConnectionError as e: print(f"连接失败:{e}") except ProtocolError as e: print(f"协议错误:{e}")
⚠️ 注意:连接失败在
async with时抛,不在构造时。所以Client(url)不会失败(只选传输),失败在async with Client(url) as ...。异常处理要包在async with外层,别包在构造上。
退出 async with 时,SDK 自动清理:
async with Client(url) as client: ... # 退出块时,SDK 自动: # 1. 关闭传输连接(关 HTTP / 杀 stdio 子进程 / 销内存分发) # 2. 清理会话状态 # 3. 释放资源
这个自动清理是 async with 的核心价值——你不用手动关连接、不用 try/finally,块退出即清理。即使块内抛异常,清理也会执行(与标准上下文管理器行为一致)。
async with 进入时依次做:打开传输 → 协商版本 → 交换能力 → 就绪。mode 参数控制协商:auto(默认,自动)、legacy(强制握手)、特定版本(强制)。async with 抛,不在构造时,异常处理包在 async with 外层。async with 自动清理连接与资源,无需手动 close。生命周期清楚了,下一节讲连接就绪后怎么调用方法。