主题
构建一个 MCP 客户端
开始构建你自己的客户端,使其能够与所有 MCP 服务端集成。
在本教程中,你将学习如何构建一个由 LLM 驱动的聊天机器人客户端,用于连接 MCP 服务端。
在开始之前,建议你先完成我们的构建一个 MCP 服务端教程,这样你就能理解客户端与服务端之间是如何通信的。
[你可以在此处找到本教程的完整代码。](https://github.com/modelcontextprotocol/quickstart-resources/tree/main/mcp-client-python)
## 系统要求
在开始之前,请确保你的系统满足以下要求:
* Mac 或 Windows 电脑
* 已安装最新版本的 Python
* 已安装最新版本的 `uv`
* 你必须使用 Python MCP SDK 2.0.0 或更高版本
## 设置你的环境
首先,使用 `uv` 创建一个新的 Python 项目:
bash
# Create project directory
uv init mcp-client
cd mcp-client
# Create virtual environment
uv venv
# Activate virtual environment
source .venv/bin/activate
# Install required packages
uv add mcp anthropic python-dotenv
# Remove boilerplate files
rm main.py
# Create our main file
touch client.pypowershell
# Create project directory
uv init mcp-client
cd mcp-client
# Create virtual environment
uv venv
# Activate virtual environment
.venv\Scripts\activate
# Install required packages
uv add mcp anthropic python-dotenv
# Remove boilerplate files
del main.py
# Create our main file
new-item client.py## 设置你的 API 密钥
你需要从 [Anthropic Console](https://console.anthropic.com/settings/keys) 获取一个 Anthropic API 密钥。
创建一个 `.env` 文件来存储它:
```bash
echo "ANTHROPIC_API_KEY=your-api-key-goes-here" > .env
```
将 `.env` 添加到你的 `.gitignore`:
```bash
echo ".env" >> .gitignore
```
WARNING
请务必妥善保管你的 ANTHROPIC_API_KEY!
## 创建客户端
### 导入与设置
首先,让我们设置导入以及文件其余部分共享的部分:
```python
import asyncio
import sys
from mcp import Client, StdioServerParameters
from mcp.client.stdio import stdio_client
from mcp_types import TextContent
from anthropic import Anthropic
from dotenv import load_dotenv
load_dotenv() # load environment variables from .env
MODEL = "claude-opus-5"
anthropic = Anthropic()
```
`Client` 是你的程序用来与服务端通信的单一对象。列出工具、调用某个工具、读取某个资源:这些都是它的方法。
### 服务端连接管理
接下来,我们将确定针对给定的服务端脚本要启动哪个进程:
```python
def server_params(server_script_path: str) -> StdioServerParameters:
"""Describe the subprocess that runs an MCP server
Args:
server_script_path: Path to the server script (.py or .js)
"""
if server_script_path.endswith(".py"):
command = "python"
elif server_script_path.endswith(".js"):
command = "node"
else:
raise ValueError("Server script must be a .py or .js file")
return StdioServerParameters(command=command, args=[server_script_path])
```
`StdioServerParameters` 是配置,而不是连接。`stdio_client()` 会把它转换为 stdio 传输,而 `Client` 会在你进入其 `async with` 块时打开该传输。我们将在 `main()` 中完成这两件事。
### 查询处理逻辑
现在让我们添加处理查询和处理工具调用的核心功能:
```python
async def process_query(client: Client, query: str) -> str:
"""Process a query using Claude and available tools"""
messages = [
{
"role": "user",
"content": query
}
]
tool_list = await client.list_tools()
available_tools = [{
"name": tool.name,
"description": tool.description,
"input_schema": tool.input_schema
} for tool in tool_list.tools]
# Initial Claude API call
response = anthropic.messages.create(
model=MODEL,
max_tokens=1000,
messages=messages,
tools=available_tools
)
# Process response and handle tool calls
final_text = []
tool_results = []
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
# Execute tool call
result = await client.call_tool(tool_name, tool_args)
final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")
tool_results.append({
"type": "tool_result",
"tool_use_id": content.id,
"content": "\n".join(
block.text
for block in result.content
if isinstance(block, TextContent)
),
"is_error": result.is_error
})
if tool_results:
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
# Get next response from Claude
response = anthropic.messages.create(
model=MODEL,
max_tokens=1000,
messages=messages,
tools=available_tools
)
for content in response.content:
if content.type == 'text':
final_text.append(content.text)
return "\n".join(final_text)
```
call_tool 返回一个 CallToolResult。它的 content 是一个块列表,这就是为什么在读取 .text 之前要先收窄为 TextContent。抛出异常的工具不会在这里抛出:它通过设置 is_error 来应答,把这个标志传递下去,就能让 Claude 读到消息并尝试其他方式。
### 交互式聊天界面
现在我们来添加聊天循环:
```python
async def chat_loop(client: Client) -> None:
"""Run an interactive chat loop"""
print("\nMCP Client Started!")
print("Type your queries or 'quit' to exit.")
while True:
try:
query = (await asyncio.to_thread(input, "\nQuery: ")).strip()
except EOFError:
break
if query.lower() == 'quit':
break
try:
response = await process_query(client, query)
print("\n" + response)
except Exception as e:
print(f"\nError: {e}")
```
`input()` 会阻塞,所以它运行在工作线程上。这样在你输入时,事件循环仍能空闲出来为连接提供服务。
### 主入口点
最后,我们添加主执行逻辑:
```python
async def main() -> None:
if len(sys.argv) < 2:
print("Usage: python client.py <path_to_server_script>")
sys.exit(1)
async with Client(stdio_client(server_params(sys.argv[1]))) as client:
tool_list = await client.list_tools()
tool_names = [tool.name for tool in tool_list.tools]
print("\nConnected to server with tools:", tool_names)
await chat_loop(client)
if __name__ == "__main__":
asyncio.run(main())
```
那个 `async with` 就是整个连接生命周期。进入它会启动服务端并与其协商协议版本;离开它会断开连接并关闭子进程。没有任何需要手动关闭的东西。
你可以[在这里](https://github.com/modelcontextprotocol/quickstart-resources/blob/main/mcp-client-python/client.py)找到完整的 `client.py` 文件。
## 关键组件详解
### 1. 客户端初始化
* 单个 `Client` 承载连接,而 `async with` 就是它的整个生命周期
* 没有需要调用的 connect/close 配对,之后也没有任何需要清理的东西
* 为 Claude 交互配置 Anthropic 客户端
### 2. 服务端连接
* 同时支持 Python 和 Node.js 服务端
* 校验服务端脚本类型
* 将服务端作为子进程启动,并通过 stdio 与其通信
* 连接打开后列出可用工具
### 3. 查询处理
* 维护对话上下文
* 处理 Claude 的响应和工具调用
* 管理 Claude 与工具之间的消息流
* 将结果组合成连贯的响应
### 4. 交互式界面
* 提供简单的命令行界面
* 处理用户输入并显示响应
* 包含基本的错误处理
* 允许优雅退出
### 5. 资源管理
* 离开 `async with` 块会断开连接并关闭服务端子进程
* 失败的查询会被报告,但不会结束会话
* 输入 `quit`,或关闭标准输入,即可干净退出
## 常见自定义点
1. **工具处理**
* 修改 `process_query()` 以处理特定工具类型
* 为工具调用添加自定义错误处理
* 实现针对特定工具的响应格式化
2. **响应处理**
* 自定义工具结果的格式化方式
* 添加响应过滤或转换
* 实现自定义日志记录
3. **用户界面**
* 添加 GUI 或 Web 界面
* 实现丰富的控制台输出
* 添加命令历史或自动补全
## 运行客户端
要使用任意 MCP 服务端运行你的客户端:
```bash
uv run client.py path/to/server.py # python server
uv run client.py path/to/build/index.js # node server
```
NOTE
如果你正在继续服务端快速入门中的天气教程,你的命令可能看起来像这样:python client.py .../quickstart-resources/weather-server-python/weather.py
客户端将会:
1. 连接到指定的服务端
2. 列出可用工具
3. 启动一个交互式聊天会话,在其中你可以:
* 输入查询
* 查看工具执行
* 获取 Claude 的响应
以下是连接到服务端快速入门中的天气服务端时它应该呈现的样子示例:
<Frame>
<img loading="lazy" src="/mcp-img/2935b5b87504.png" width="1932" height="1739" data-path="images/client-claude-cli-python.png" />
</Frame>
## 工作原理
当你提交查询时:
1. 客户端从服务端获取可用工具列表
2. 你的查询连同工具描述一起发送给 Claude
3. Claude 决定使用哪些工具(如果有的话)
4. 客户端通过服务端执行任何被请求的工具调用
5. 结果被发送回 Claude
6. Claude 提供自然语言响应
7. 响应显示给你
## 最佳实践
1. **错误处理**
* 检查 `result.is_error`,而不是期望失败的工具抛出异常
* 提供有意义的错误消息
* 优雅地处理连接问题
2. **资源管理**
* 让 `async with` 块拥有连接
* 在你需要服务端期间保持其打开
* 处理服务端断开连接
安全
- 将 API 密钥安全地存储在
.env中 - 验证服务端响应
- 谨慎对待工具权限
- 将 API 密钥安全地存储在
工具名称
- 工具名称可按照此处指定的格式进行验证
- 如果工具名称符合指定格式,则不应在 MCP 客户端验证中失败
故障排查
服务端路径问题
- 仔细检查服务端脚本的路径是否正确
- 如果相对路径不起作用,请使用绝对路径
- 对于 Windows 用户,请确保在路径中使用正斜杠 (/) 或转义反斜杠 (\)
- 验证服务端文件具有正确的扩展名(Python 为 .py,Node.js 为 .js)
正确路径用法示例:
bash
# Relative path
uv run client.py ./server/weather.py
# Absolute path
uv run client.py /Users/username/projects/mcp-server/weather.py
# Windows path (either format works)
uv run client.py C:/projects/mcp-server/weather.py
uv run client.py C:\\projects\\mcp-server\\weather.py响应时机
- 首次响应可能需要长达 30 秒才能返回
- 这是正常现象,发生在以下情况:
- 服务端初始化时
- Claude 处理查询时
- 工具正在执行时
- 后续响应通常更快
- 在此初始等待期间不要中断进程
常见错误消息
如果你看到:
FileNotFoundError:检查你的服务端路径Connection refused:确保服务端正在运行且路径正确Tool execution failed:验证工具的必需环境变量已设置Timeout error:考虑提高Client上的read_timeout_seconds