1. 客户端 SDK 的职责
MCP 客户端 SDK 封装了协议的「对话细节」,让应用只关心「连接 + 调用工具」。它要处理:传输建立、协议握手、请求-响应、通知、错误与重试。
一句话:客户端 SDK 是「协议的死记硬背层」——应用不用懂 JSON-RPC 细节,SDK 负责把它翻译成可靠的调用。
1.1 SDK 的抽象边界
应用层(业务):
const result = await client.callTool({ name, args })
SDK 层(协议):
- 建立传输(stdio / HTTP)
- 初始化握手(initialize / initialized)
- 发送 JSON-RPC 请求 / 处理响应
- 处理通知与错误
传输层:
- stdio 双向管道
- Streamable HTTP 会话
2. Client 类与连接生命周期
2.1 创建与连接
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
// 1. 创建客户端(声明能力)
const client = new Client({
name: "my-agent",
version: "1.0.0",
capabilities: {
sampling: {}, // 客户端支持 sampling
roots: {}, // 客户端支持 roots
},
});
// 2. 建立传输
const transport = new StdioClientTransport({
command: "node",
args: ["server.js"],
});
// 3. 连接(内部完成 initialize 握手)
await client.connect(transport);
2.2 握手状态机
connect()
→ transport.start() // 建立通道
→ initialize // 客户端→服务器:能力声明
→ initialized // 服务器→客户端:确认
→ connected // 可调用工具/读资源
→ close() // 关闭,进入 terminated
状态:disconnected → connecting → connected → terminating → terminated
2.3 能力协商
initialize 时双方交换 capabilities:
服务器:tools、resources、prompts、logging
客户端:sampling、roots
协商结果决定「哪些请求可用」
一句话:connect 不是「连上管道」就完事,而是走完 initialize 握手、完成能力协商后才真正可用——状态机是客户端 SDK 的骨架。
3. 核心 API 调用
3.1 调用工具
const result = await client.callTool({
name: "search",
arguments: { query: "MCP 鉴权", limit: 10 },
});
// result 是 JSON-RPC 结果
// result.isError = false
// result.content = [ { type: "text", text: "..." } ]
3.2 读取资源
// 读取单个资源
const res = await client.readResource({ uri: "config://app/settings" });
// 列出资源
const list = await client.listResources({ cursor });
// 列出提示词
const prompts = await client.listPrompts();
3.3 获取工具列表
// 拉取服务器全部工具定义(JSON Schema)
const tools = await client.listTools();
// tools.tools: [{ name, description, inputSchema }]
3.4 并行与顺序
// 并行调用(互不依赖)
const [a, b] = await Promise.all([
client.callTool({ name: "ping", arguments: {} }),
client.callTool({ name: "status", arguments: {} }),
]);
// 顺序调用(依赖前一步结果)
const r1 = await client.callTool({ name: "query", arguments: { id: 1 } });
const r2 = await client.callTool({ name: "detail", arguments: { id: r1.id } });
一句话:核心 API 就三类——callTool 做动作、readResource 取数据、listTools/listPrompts 摸能力;并行与顺序由 Agent 的决策编排决定。
4. 通知与请求-响应
4.1 三种消息类型
Request :请求-响应(callTool 等)——需要应答
Notification:单向通知(如 resources/listChanged)——无需应答
Response :对请求的应答
协议保证:请求按 id 关联,响应带对应 id
4.2 客户端处理通知
// 订阅资源变化 → 服务器发 listChanged 通知 → 重新拉取
client.setRequestHandler(ListChangedNotificationSchema, async () => {
const res = await client.listResources();
updateUi(res.resources);
});
4.3 请求 id 与并发
- 每个请求带唯一 id(递增)
- 服务器可乱序返回,客户端按 id 匹配
- 并发请求上限由客户端控制
- 通知不带 id(与请求区分)
一句话:请求/响应/通知三类消息 + id 关联,构成了「并发可乱序、通知独立推」的协议语义——客户端 SDK 按 id 分发,Agent 无需串行化所有调用。
5. 传输层管理
5.1 StdioClientTransport
- 子进程管理(spawn、信号、退出码)
- 双向管道(stdin 写、stdout 读)
- stderr 透传日志
- close → kill 进程
5.2 StreamableClientTransport
- HTTP POST /message(JSON-RPC 体)
- SSE 流式接收
- 会话管理(session id / 无状态模式)
- 心跳 / 超时
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const transport = new StreamableHTTPClientTransport(
new URL("https://mcp.example.com/mcp"),
{ authProvider } // 可选:OAuth 令牌提供者
);
await client.connect(transport);
5.3 连接可靠性
- 传输断开 → 监听 onclose → 重连策略
- 指数退避重试(避免打爆服务器)
- 请求超时 → 可重试(幂等操作)
- 会话丢失 → 重新 initialize
一句话:传输层决定了「怎么把 JSON-RPC 送出去」——stdio 管进程、HTTP 管网络;可靠连接靠重连、退避、超时三板斧。
6. 错误分类与重试
6.1 JSON-RPC 错误码
| 错误码 | 含义 | 处理 |
|---|---|---|
| -32700 | 解析错误 | 客户端 bug,不重试 |
| -32600 | 无效请求 | 请求格式错误,修请求 |
| -32601 | 方法不存在 | 服务器不支持,报错 |
| -32602 | 无效参数 | 校验参数后重试 |
| -32603 | 内部错误 | 可重试(退避) |
| -32000+ | 服务器自定义 | 按具体错误处理 |
6.2 工具执行错误
result.isError = true 是「业务失败」,不是协议错误
→ 向 LLM 返回错误文本,让模型调整后重试
协议错误(抛异常)→ 区分可重试与不可重试
6.3 重试策略
// 幂等操作可安全重试
async function callWithRetry(fn, { retries = 3, base = 200 }) {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (e) {
if (isNonRetriable(e)) throw e;
await sleep(base * 2 ** i); // 指数退避
}
}
throw new Error("retry exhausted");
}
6.4 超时与取消
// 带超时的调用
const result = await Promise.race([
client.callTool({ name: "search", arguments: args }),
timeout(10_000), // 10s 超时
]);
一句话:错误处理的核心是「区分业务失败与协议失败、可重试与不可重试」——isError 交给 LLM 决策,协议异常走退避重试,超时兜底防挂死。
7. Python SDK 对应实现
from mcp.client.stdio import stdio_client
from mcp.client.session import ClientSession
async def main():
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
# 握手
await session.initialize()
# 调用工具
result = await session.call_tool(
"search", {"query": "MCP", "limit": 5}
)
# 读取资源
content = await session.read_resource("config://app/settings")
Python SDK 对称 API:
call_tool / read_resource / list_tools / list_resources
通知处理:session.set_*_notification_handler
传输:stdio_client / streamable_http_client
一句话:TS 与 Python SDK 是同一协议的两套实现——API 对称、语义一致,选型看应用栈,不必重学协议。
8. 常见陷阱与排障
| 陷阱 | 症状 | 解决 |
|---|---|---|
| 未初始化就 callTool | 协议错误 | connect 后再调用 |
| 并发无上限 | 服务器过载 | 客户端并发池 |
| 无超时 | 调用挂死 | 统一 timeout |
| 重试非幂等操作 | 副作用重复 | 仅幂等可重试 |
| 忽略 onclose | 断连无感 | 监听重连 |
| 未处理通知 | 资源更新丢失 | 注册 notification handler |
调试技巧:
- 开启 SDK 的调试日志(transport 层打印 JSON-RPC)
- 用 mcp-inspector 单步观察请求/响应
- 抓异常堆栈区分「协议错误 vs 业务错误」
9. 总结
MCP 客户端 SDK 的要点可以概括为「连得上、调得稳、错得清」:
| 层面 | 要点 |
|---|---|
| 连接 | initialize 握手 + 能力协商,状态机驱动 |
| 调用 | callTool / readResource / listTools 三类核心 |
| 消息 | 请求带 id、响应按 id 匹配、通知独立推 |
| 传输 | stdio 管进程、HTTP 管网络,重连 + 退避 |
| 错误 | isError 交 LLM、协议错分可重试、超时兜底 |
| 双语言 | TS / Python SDK 对称实现 |
客户端 SDK 是 Agent 与 MCP 服务器之间的「协议翻译官」:把状态机、消息关联、传输、错误处理这些细节全部藏起来,让上层只关心「调哪个工具」。掌握 SDK 内部,才能在遇到诡异的连接问题时,快速定位是协议、传输还是业务问题。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。