MCP 客户端开发
本章详细介绍如何开发一个 MCP Client,从环境搭建、协议接入到实现与服务器的交互。
客户端角色概述
在 MCP 架构中,Client(通常是 AI 应用程序或其一部分)负责:
- 管理连接:与一或多个 MCP Server 建立并维护连接。
- 状态同步:请求并获取能力列表,监听能力的变化通知。
- 数据交换:将大模型(LLM)的诉求转化为对 Server 的调用(如执行工具、读取资源)。
- 安全与防线:在执行敏感操作(如调用破坏性工具)前对用户进行提示与确认。
通过开发客户端接入支持 MCP 的系统,你可以极大地扩展 AI 的能力边界,使其真正成为能够操作外部世界的智能体。
环境准备
选项 1:Node.js / TypeScript
# 初始化项目
mkdir my-mcp-client && cd my-mcp-client
npm init -y
npm install @modelcontextprotocol/sdk
npm install -D typescript @types/node tsx
npx tsc --init
选项 2:Python
# 创建并激活环境
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# 安装 SDK
pip install mcp
连接到服务器
MCP 支持两种主要的传输协议:Stdio(标准输入输出) 和 SSE(服务器发送事件,基于 HTTP)。
1. 使用 Stdio 连接本地服务器
Stdio 适用于本地环境,客户端将服务器作为一个子进程启动。
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
async function runClient() {
// 定义要启动的服务器命令及参数
const transport = new StdioClientTransport({
command: "node",
args: ["/path/to/your/mcp-server/dist/index.js"]
});
// 创建客户端实例
const client = new Client(
{ name: "my-mcp-client", version: "1.0.0" },
{ capabilities: {} } // 声明客户端能力
);
// 建立连接
console.log("正在连接到服务器...");
await client.connect(transport);
console.log("连接成功!");
// 接下来的操作...
}
runClient().catch(console.error);
2. 使用 SSE 连接远程服务器
SSE 适用于需要访问远程服务器的场景。
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
async function runSSEClient() {
// 创建连接到远程服务器的 SSE 传输通道
const transport = new SSEClientTransport(
new URL("https://api.example.com/mcp/sse"),
{
// 可以在此处添加鉴权头
headers: {
Authorization: "Bearer your-token-here"
}
}
);
const client = new Client(
{ name: "my-mcp-client", version: "1.0.0" },
{ capabilities: {} }
);
await client.connect(transport);
console.log("远程服务器连接成功!");
}
核心操作
连接建立并完成初始化握手之后,客户端就可以操作核心原语了。
发现与调用工具 (Tools)
大模型可以借助工具系统操作系统资源或请求外部接口。客户端职责之一就是列出可用的工具,并根据需要调用它们。
async function handleTools(client: Client) {
// 1. 获取服务器提供的工具列表
const response = await client.listTools();
console.log("可用工具:", response.tools.map(t => t.name).join(", "));
// 这份列表通常会告诉 LLM 有哪些能力。
// 假设 LLM 决定调用名为 "get_weather" 的工具
const toolName = "get_weather";
const toolArgs = { location: "Shanghai" };
// 2. 调用工具
try {
const result = await client.callTool({
name: toolName,
arguments: toolArgs
});
// 3. 处理返回的内容
if (result.isError) {
console.error("工具调用失败:", result.content);
} else {
console.log("工具调用成功:", result.content);
}
} catch (error) {
console.error("请求异常:", error);
}
}
发现与读取资源 (Resources)
资源为客户端提供了可以直接读取的静态或动态数据,例如当前正编辑的文件内容。
async function handleResources(client: Client) {
// 1. 获取可用资源列表
const response = await client.listResources();
console.log("找到资源数量:", response.resources.length);
if (response.resources.length > 0) {
// 2. 尝试读取第一个资源
const resourceUri = response.resources[0].uri;
const result = await client.readResource({ uri: resourceUri });
// 资源可能是文本,也可能是 base64 编码的二进制数据
for (const content of result.contents) {
if ((content as any).text) {
console.log(`资源 [${content.uri}] 文本内容:`, (content as any).text);
} else if ((content as any).blob) {
console.log(`资源 [${content.uri}] 二进制数据 (base64)`);
}
}
}
}
查询与使用提示 (Prompts)
提示模板系统允许服务器提供预先设定好的系统交互 prompt,客户端直接拿来使用。
async function handlePrompts(client: Client) {
// 1. 获取提示列表
const response = await client.listPrompts();
console.log("可用提示模板:", response.prompts.map(p => p.name));
// 2. 获取具体提示内容
if (response.prompts.length > 0) {
const promptName = response.prompts[0].name;
const promptDetails = await client.getPrompt({
name: promptName,
arguments: {
// 如果模板有必需的参数需要传入
}
});
console.log("提示模板内容:", promptDetails.description);
promptDetails.messages.forEach(msg => {
console.log(`[${msg.role}]:`, msg.content);
});
}
}
订阅与通知处理
除了主动请求数据,客户端还可以监听服务器的实时通知。
// 监听工具列表变化
client.setNotificationHandler("notifications/tools/list_changed", () => {
console.log("注意到服务器的工具列表发生变化,应重新调用 listTools() 同步状态!");
});
// 监听资源更新
client.setNotificationHandler("notifications/resources/updated", (notification) => {
// 需要强制类型推断,或者依赖确切的类型定义
const uri = (notification.params as any).uri;
console.log(`资源更新: ${uri}`);
});
// 监听日志消息
client.setNotificationHandler("notifications/message", (notification) => {
const params = notification.params as any;
console.log(`[Server Log -> ${params.level}]: ${params.data}`);
});
大模型接入实战(伪代码)
一个典型的 AI MCP Client 完整工作流如下所示:
// 伪代码演示:结合 LLM 与 MCP Clinet 的核心交互循环
async function aiWorkflow(userMessage: string) {
// 1. 从 MCP 获取所有可用工具架构 ( JSON Schema )
const toolsResponse = await client.listTools();
const availableTools = toolsResponse.tools.map(tool => ({
name: tool.name,
description: tool.description,
schema: tool.inputSchema
}));
// 2. 发送用户消息并附加可用工具给大模型
let messages = [ { role: 'user', content: userMessage } ];
let llmResponse = await llm.chat({
messages,
tools: availableTools
});
// 3. 循环处理模型工具调用
while (llmResponse.tool_calls) {
const call = llmResponse.tool_calls[0];
// 安全起见,客户端应拦截危险操作并要求用户确认
if (call.name === "delete_database") {
const userOk = await promptUser("模型提议删除数据库,是否允许?");
if (!userOk) throw new Error("用户取消了敏感操作。");
}
// 执行对应的 MCP 工具
const mcpResult = await client.callTool({
name: call.name,
arguments: JSON.parse(call.arguments)
});
// 4. 将工具执行结果送回给大模型
messages.push(llmResponse.message); // 模型上一轮的响应内容
messages.push({
role: 'tool',
tool_call_id: call.id,
content: JSON.stringify(mcpResult.content)
});
// 模型结合执行结果继续思考
llmResponse = await llm.chat({
messages,
tools: availableTools
});
}
// 5. 将最终文本返回给用户
console.log("AI:", llmResponse.content);
}
Python 客户端 SDK 示例
如果你的 AI 后端是以 Python 编写的,同样可以使用官方的 Python SDK:
import asyncio
from mcp import ClientSession
from mcp.client.stdio import stdio_client, StdioServerParameters
async def main():
# 设定要连接的服务器运行参数
server_params = StdioServerParameters(
command="node",
args=["/path/to/server/index.js"]
)
# 启动进程并通过 stdio 建立连接
async with stdio_client(server_params) as (read_stream, write_stream):
# 创建客户端会话
async with ClientSession(read_stream, write_stream) as session:
# 执行初始化握手
await session.initialize()
# 列出可用资源
resources = await session.list_resources()
print("Found resources:", [r.uri for r in resources])
# 列出可用工具
tools = await session.list_tools()
print("Found tools:", [t.name for t in tools])
# 调用特定工具
if any(t.name == "get_weather" for t in tools):
result = await session.call_tool("get_weather", arguments={"location": "Beijing"})
print("Tool result:", result)
if __name__ == "__main__":
asyncio.run(main())
最佳实践与注意事项
1. 完善的安全拦截
正如前面提到,AI 本身不具备准确的风险判定能力,当调用具有破坏性的 API 时(destructiveHint: true),客户端必须作为最后一道防线:
- 在界面直观提示用户即将进行的操作详情及影响。
- 等待用户主动点击确认授权,绝不默认放行。
2. 长时间运行超时处理
部分工具和资源调用可能会引发长耗时阻塞。配置完善的超时机制(Timeout)和请求取消能力非常重要,以防客户端假死:
- 为 MCP 客户端
listTools、callTool设定合理的超时上下文。
3. 重连机制
当处于微服务环境下并使用 SSE 连接远程 MCP Server 时,网络抖动随时可能发生。必须具备健全的重试(Retries)和短线重连策略,保证上下文服务的稳定与连续。
小结
本章学习了:
- 客户端角色:职责与工作边界。
- 连接策略:Stdio 进程通信与 SSE 远程通信的区别和建立方法。
- 调用核心原语:发现、调用并处理 Tools, Resources, Prompts 数据。
- 事件监听:处理由 Server 发起的变更通知。
- AI 架构融合:构建典型的 LLM -> Client -> Server 工作控制流。
- 常见语言支持:掌握 TypeScript 与 Python 两种开发语言下的 SDK 用法。
参考资料
练习
- 在本地创建一个 Node.js 客户端并使用
stdio连接之前章节中开发的服务器。 - 试着编写一个客户端,每隔一分钟请求一次
listTools检查可用工具。 - 结合 OpenAI 或 Anthropic API,用 Python 实现一段调用 MCP 工具的完整 AI 对话脚本。