跳到主要内容

MCP 客户端开发

本章详细介绍如何开发一个 MCP Client,从环境搭建、协议接入到实现与服务器的交互。

客户端角色概述

在 MCP 架构中,Client(通常是 AI 应用程序或其一部分)负责:

  1. 管理连接:与一或多个 MCP Server 建立并维护连接。
  2. 状态同步:请求并获取能力列表,监听能力的变化通知。
  3. 数据交换:将大模型(LLM)的诉求转化为对 Server 的调用(如执行工具、读取资源)。
  4. 安全与防线:在执行敏感操作(如调用破坏性工具)前对用户进行提示与确认。

通过开发客户端接入支持 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 客户端 listToolscallTool 设定合理的超时上下文。

3. 重连机制

当处于微服务环境下并使用 SSE 连接远程 MCP Server 时,网络抖动随时可能发生。必须具备健全的重试(Retries)和短线重连策略,保证上下文服务的稳定与连续。

小结

本章学习了:

  1. 客户端角色:职责与工作边界。
  2. 连接策略:Stdio 进程通信与 SSE 远程通信的区别和建立方法。
  3. 调用核心原语:发现、调用并处理 Tools, Resources, Prompts 数据。
  4. 事件监听:处理由 Server 发起的变更通知。
  5. AI 架构融合:构建典型的 LLM -> Client -> Server 工作控制流。
  6. 常见语言支持:掌握 TypeScript 与 Python 两种开发语言下的 SDK 用法。

参考资料

练习

  1. 在本地创建一个 Node.js 客户端并使用 stdio 连接之前章节中开发的服务器。
  2. 试着编写一个客户端,每隔一分钟请求一次 listTools 检查可用工具。
  3. 结合 OpenAI 或 Anthropic API,用 Python 实现一段调用 MCP 工具的完整 AI 对话脚本。