话接上文:一文读懂如何使用MCP创建服务器
本篇文章我们接着讲MCP客户端的创建过程,参考MCP官方资料
如何创建MCP客户端
开始构建自己的客户端,该客户端可以与所有 MCP 服务器集成。
在本教程中,您将学习如何构建一个由 LLM 驱动的聊天机器人客户端,该客户端可以连接到 MCP 服务器。最好先完成一次服务器快速入门 ,该教程将引导您完成构建第一个服务器的基本步骤。
系统要求
开始之前,请确保您的系统满足以下要求:
- Mac 或 Windows 电脑
- 已安装的最新 Python 版本
- 已安装 uv 的最新版本
设置环境
首先,使用 uv 创建一个新的 Python 项目:
# 项目初始化
uv init mcp-client
cd mcp-client
# 创建虚拟环境
uv venv
# 激活虚拟环境
# On Windows:
.venv\Scripts\activate
# On Unix or MacOS:
source .venv/bin/activate
# 安装工具库
uv add mcp anthropic python-dotenv
# 删除无用main.py
# On Windows:
del main.py
# On Unix or MacOS:
rm main.py
# 创建客户端脚本
touch client.py
设置您的 API 密钥
您需要从 Anthropic 控制台获取 Anthropic API 密钥。
创建一个 .env 文件来存储它:
# Create .env file
touch .env
将你的密钥添加到 .env 文件中:
ANTHROPIC_API_KEY=<your key here>
将 .env 添加到你的 .gitignore 中:
echo ".env" >> .gitignore
创建客户端
导入客户端的依赖库
首先,让我们设置导入并创建基本的客户端类:
import asyncio # 异步IO模块,用于处理异步任务
from typing import Optional # 类型注解支持,表示可选类型
from contextlib import AsyncExitStack # 异步上下文管理器,用于安全地管理资源释放
# 导入 MCP 协议相关模块
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client # 基于标准输入输出的客户端通信方式
# 导入 Anthropic 提供的 Python SDK,用于访问 Claude 模型
from anthropic import Anthropic
# 从 dotenv 加载环境变量(例如 API Key)
from dotenv import load_dotenv
# 从 .env 文件中加载环境变量(如 API 密钥等敏感信息)
load_dotenv()
class MCPClient:
def __init__(self):
# 初始化会话和资源管理对象
self.session: Optional[ClientSession] = None # 存储与 MCP 服务器的会话对象
self.exit_stack = AsyncExitStack() # 异步资源管理栈,确保资源正确释放
self.anthropic = Anthropic() # 创建 Anthropic 客户端实例,用于调用 Claude 模型
服务器连接管理
接下来,我们将实现连接到 MCP 服务器的方法:
async def connect_to_server(self, server_script_path: str):
"""连接到 MCP 服务器
参数:
server_script_path: 服务器脚本路径(.py 或 .js 文件)
"""
# 判断服务器脚本是 Python 还是 JavaScript 文件
is_python = server_script_path.endswith('.py')
is_js = server_script_path.endswith('.js')
# 如果不是 .py 或 .js 文件,抛出异常
if not (is_python or is_js):
raise ValueError("Server script must be a .py or .js file")
# 根据文件类型选择执行命令:Python 或 Node.js
command = "python" if is_python else "node"
# 构建服务器启动参数
server_params = StdioServerParameters(
command=command, # 执行命令
args=[server_script_path], # 启动参数,即服务器脚本路径
env=None # 环境变量,使用当前进程的环境变量
)
# 使用 stdio_client 启动服务器并建立通信管道
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
self.stdio, self.write = stdio_transport # 保存 stdin/stdout 和写入函数
# 创建 ClientSession 实例,并绑定通信通道
self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
# 初始化会话(发送初始化请求)
await self.session.initialize()
# 获取服务器提供的可用工具列表
response = await self.session.list_tools()
tools = response.tools # 保存返回的工具列表
print("\nConnected to server with tools:", [tool.name for tool in tools])
查询处理逻辑
现在让我们添加处理查询和处理工具调用的核心功能,示例客户端的核心逻辑就在这里
async def process_query(self, query: str) -> str:
"""Process a query using Claude and available tools"""
# 初始化消息列表,包含用户的查询内容
messages = [
{
"role": "user",
"content": query
}
]
# 获取可用的工具列表
response = await self.session.list_tools()
# 将响应中的工具信息提取出来,构建一个包含工具名称、描述和输入模式的字典列表
available_tools = [{
"name": tool.name,
"description": tool.description,
"input_schema": tool.inputSchema
} for tool in response.tools]
# 第一次调用Claude API,发送消息并获取响应
response = self.anthropic.messages.create(
model="claude-3-5-sonnet-20241022", # 使用的AI模型
max_tokens=1000, # 最大生成token数
messages=messages, # 消息列表
tools=available_tools # 可用工具列表
)
# 处理响应并处理工具调用
final_text = [] # 存储最终输出的文本
# 遍历响应内容
for content in response.content:
if content.type == 'text': # 如果是纯文本
final_text.append(content.text) # 添加到最终文本列表中
elif content.type == 'tool_use': # 如果是工具调用
tool_name = content.name # 工具名称
tool_args = content.input # 工具参数
# 执行工具调用
result = await self.session.call_tool(tool_name, tool_args)
# 记录调用工具的信息
final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")
# 如果有文本内容,则将其添加到消息列表中
if hasattr(content, 'text') and content.text:
messages.append({
"role": "assistant",
"content": content.text
})
# 将工具调用结果添加到消息列表中
messages.append({
"role": "user",
"content": result.content
})
# 再次调用Claude API,使用更新后的消息列表获取下一轮响应
response = self.anthropic.messages.create(
model="claude-3-5-sonnet-20241022", # 使用的AI模型
max_tokens=1000, # 最大生成token数
messages=messages, # 更新后的消息列表
)
# 将新的响应内容添加到最终文本列表中
final_text.append(response.content[0].text)
return "\n".join(final_text) # 返回所有文本内容的拼接结果
交互式聊天界面
现在我们将添加聊天循环和清理功能:
async def chat_loop(self):
"""运行一个交互式的聊天循环"""
# 提示用户客户端已启动,可以输入问题或输入 'quit' 退出
print("\nMCP Client Started!")
print("Type your queries or 'quit' to exit.")
# 开始无限循环,持续接收用户输入
while True:
try:
# 提示用户输入查询内容,并去除前后空格
query = input("\nQuery: ").strip()
# 如果用户输入 "quit"(不区分大小写),则退出循环
if query.lower() == 'quit':
break
# 调用 process_query 方法处理用户输入,获取响应
response = await self.process_query(query)
# 打印模型返回的响应结果
print("\n" + response)
except Exception as e:
# 捕获并打印任何异常信息
print(f"\nError: {str(e)}")
async def cleanup(self):
"""清理资源"""
# 关闭异步资源管理栈,释放所有占用的资源(如关闭子进程、网络连接等)
await self.exit_stack.aclose()
主入口点
最后,我们将添加主要的执行逻辑:
async def main():
# 检查命令行参数是否提供了服务器脚本路径,如果没有则打印使用说明并退出
if len(sys.argv) < 2:
print("Usage: python client.py <path_to_server_script>")
sys.exit(1)
# 创建 MCPClient 实例
client = MCPClient()
# 尝试连接到服务器并进入聊天循环
try:
await client.connect_to_server(sys.argv[1]) # 使用提供的服务器脚本路径连接服务器
await client.chat_loop() # 进入与服务器的持续交互循环
finally:
await client.cleanup() # 确保在程序结束时执行清理操作(如关闭连接)
# 如果该文件作为主程序运行,则执行以下内容
if __name__ == "__main__":
import sys # 导入sys模块,用于访问解释器相关的变量和函数
asyncio.run(main()) # 使用asyncio运行main协程函数,启动事件循环
你可以在这里找到完整的 client.py 文件 。带中文注释的如下:
import asyncio # 异步IO模块,用于处理异步任务
from typing import Optional # 类型注解支持,表示可选类型
from contextlib import AsyncExitStack # 异步上下文管理器,用于安全地管理资源释放
# 导入 MCP 协议相关模块
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client # 基于标准输入输出的客户端通信方式
# 导入 Anthropic 提供的 Python SDK,用于访问 Claude 模型
from anthropic import Anthropic
# 从 dotenv 加载环境变量(例如 API Key)
from dotenv import load_dotenv
# 从 .env 文件中加载环境变量(如 API 密钥等敏感信息)
load_dotenv()
class MCPClient:
def __init__(self):
# 初始化会话和资源管理对象
self.session: Optional[ClientSession] = None # 存储与 MCP 服务器的会话对象
self.exit_stack = AsyncExitStack() # 异步资源管理栈,确保资源正确释放
self.anthropic = Anthropic() # 创建 Anthropic 客户端实例,用于调用 Claude 模型
async def connect_to_server(self, server_script_path: str):
"""连接到 MCP 服务器
参数:
server_script_path: 服务器脚本路径(.py 或 .js 文件)
"""
# 判断服务器脚本是 Python 还是 JavaScript 文件
is_python = server_script_path.endswith('.py')
is_js = server_script_path.endswith('.js')
# 如果不是 .py 或 .js 文件,抛出异常
if not (is_python or is_js):
raise ValueError("Server script must be a .py or .js file")
# 根据文件类型选择执行命令:Python 或 Node.js
command = "python" if is_python else "node"
# 构建服务器启动参数
server_params = StdioServerParameters(
command=command, # 执行命令
args=[server_script_path], # 启动参数,即服务器脚本路径
env=None # 环境变量,使用当前进程的环境变量
)
# 使用 stdio_client 启动服务器并建立通信管道
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
self.stdio, self.write = stdio_transport # 保存 stdin/stdout 和写入函数
# 创建 ClientSession 实例,并绑定通信通道
self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
# 初始化会话(发送初始化请求)
await self.session.initialize()
# 获取服务器提供的可用工具列表
response = await self.session.list_tools()
tools = response.tools # 保存返回的工具列表
print("\nConnected to server with tools:", [tool.name for tool in tools])
async def process_query(self, query: str) -> str:
"""Process a query using Claude and available tools"""
# 初始化消息列表,包含用户的查询内容
messages = [
{
"role": "user",
"content": query
}
]
# 获取可用的工具列表
response = await self.session.list_tools()
# 将响应中的工具信息提取出来,构建一个包含工具名称、描述和输入模式的字典列表
available_tools = [{
"name": tool.name,
"description": tool.description,
"input_schema": tool.inputSchema
} for tool in response.tools]
# 第一次调用Claude API,发送消息并获取响应
response = self.anthropic.messages.create(
model="claude-3-5-sonnet-20241022", # 使用的AI模型
max_tokens=1000, # 最大生成token数
messages=messages, # 消息列表
tools=available_tools # 可用工具列表
)
# 处理响应并处理工具调用
final_text = [] # 存储最终输出的文本
# 遍历响应内容
for content in response.content:
if content.type == 'text': # 如果是纯文本
final_text.append(content.text) # 添加到最终文本列表中
elif content.type == 'tool_use': # 如果是工具调用
tool_name = content.name # 工具名称
tool_args = content.input # 工具参数
# 执行工具调用
result = await self.session.call_tool(tool_name, tool_args)
# 记录调用工具的信息
final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")
# 如果有文本内容,则将其添加到消息列表中
if hasattr(content, 'text') and content.text:
messages.append({
"role": "assistant",
"content": content.text
})
# 将工具调用结果添加到消息列表中
messages.append({
"role": "user",
"content": result.content
})
# 再次调用Claude API,使用更新后的消息列表获取下一轮响应
response = self.anthropic.messages.create(
model="claude-3-5-sonnet-20241022", # 使用的AI模型
max_tokens=1000, # 最大生成token数
messages=messages, # 更新后的消息列表
)
# 将新的响应内容添加到最终文本列表中
final_text.append(response.content[0].text)
return "\n".join(final_text) # 返回所有文本内容的拼接结果
async def chat_loop(self):
"""运行一个交互式的聊天循环"""
# 提示用户客户端已启动,可以输入问题或输入 'quit' 退出
print("\nMCP Client Started!")
print("Type your queries or 'quit' to exit.")
# 开始无限循环,持续接收用户输入
while True:
try:
# 提示用户输入查询内容,并去除前后空格
query = input("\nQuery: ").strip()
# 如果用户输入 "quit"(不区分大小写),则退出循环
if query.lower() == 'quit':
break
# 调用 process_query 方法处理用户输入,获取响应
response = await self.process_query(query)
# 打印模型返回的响应结果
print("\n" + response)
except Exception as e:
# 捕获并打印任何异常信息
print(f"\nError: {str(e)}")
async def cleanup(self):
"""清理资源"""
# 关闭异步资源管理栈,释放所有占用的资源(如关闭子进程、网络连接等)
await self.exit_stack.aclose()
async def main():
# 检查命令行参数是否提供了服务器脚本路径,如果没有则打印使用说明并退出
if len(sys.argv) < 2:
print("Usage: python client.py <path_to_server_script>")
sys.exit(1)
# 创建 MCPClient 实例
client = MCPClient()
# 尝试连接到服务器并进入聊天循环
try:
await client.connect_to_server(sys.argv[1]) # 使用提供的服务器脚本路径连接服务器
await client.chat_loop() # 进入与服务器的持续交互循环
finally:
await client.cleanup() # 确保在程序结束时执行清理操作(如关闭连接)
# 如果该文件作为主程序运行,则执行以下内容
if __name__ == "__main__":
import sys # 导入sys模块,用于访问解释器相关的变量和函数
asyncio.run(main()) # 使用asyncio运行main协程函数,启动事件循环
运行客户端
要在任何 MCP 服务器上运行您的客户端:
uv run client.py path/to/server.py # python server
uv run client.py path/to/build/index.js # node server
如果您要从服务器快速入门继续气象教程,您的命令可能看起来像这样: python client.py …/quickstart-resources/weather-server-python/weather.py
客户端将:
- 连接到指定的服务器
- 列出可用的工具
- 开始一个互动聊天会话,您可以:
- 输入查询
- 查看工具执行
- 从 Claude 获取响应
这里是从服务器快速入门连接到天气服务器后应该看起来的样子:
客户端的工作原理
MCP 架构参考图:
当您提交查询时:
- 客户端从服务器获取可用工具列表
- 您的查询连同工具描述一起发送给 Claude大模型
- Claude大模型 决定使用哪些工具(如果有)
- 客户通过服务器执行任何请求的工具调用
- 工具响应结果发送回 Claude大模型
- Claude大模型提供自然语言响应
- 响应显示给您
未完待续。欢迎关注,下一期介绍:如何不自定义客户端和服务器,用户如何使用MCP?