使用说明
从头开始构建MCP客户端:完整指南
目录
- 简介&你将构建什么
- 先决条件
- 快速入门(适用于经验丰富的开发人员)
- 从头开始详细设置
- 代码架构概述
- 详细代码演练
- 使用不同的LLM提供者
- 运行客户端
- 测试您的设置
- 故障排除
- 项目结构参考
- 后续步骤和扩展
- 额外资源
简介&你将构建什么
什么是MCP(模型上下文协议)?
这 模型上下文协议(MCP) 是一个开放协议,规范了应用程序如何向大型语言模型(LLM)提供上下文。把它想象成一个通用的翻译器,让人工智能助手(如Claude、ChatGPT或Gemini)以一致的方式与外部工具、数据库和服务进行交互。
这个客户在做什么
此MCP客户端是 对话式人工智能界面 即:
- 🔌 连接到MCP服务器 提供工具(如文件系统、数据库、API)
- 💬 维护对话历史记录 用于上下文感知交互
- 🛠️ 执行工具调用 当LLM需要时自动执行
- 🔄 处理多回合对话 智能跟进
- 🎯 与多家LLM提供商合作 (双子座、克劳德或其他)
主要特点
| 特性 | 描述 |
|---|---|
| 异步架构 | 无阻塞I/O,可与MCP服务器高效通信 |
| 工具调用循环 | 在一次对话中自动执行多个工具调用 |
| 对话记忆 | 维护完整的对话历史记录,以获取上下文响应 |
| 供应商不可知 | 易于在不同的LLM提供商(Gemini、Claude等)之间交换 |
| 错误处理 | 强大的错误恢复和信息丰富的错误消息 |
| 交互式CLI | 用于测试和探索的简单命令行界面 |
先决条件
所需软件
-
Python 3.11或更高版本 (3.13推荐)
- 检查您的版本:
python --version或python3 --version - 下载地址: python.org
- 检查您的版本:
-
uv包管理器 (强烈推荐)
- 为什么是紫外线? 它比pip快10-100倍,具有更好的依赖性解决方案,并使虚拟环境管理无缝衔接。
- 在Windows上安装:
powershell -c "irm https://astral.sh/uv/install.ps1 | iex" - 在macOS/Linux上安装:
curl -LsSf https://astral.sh/uv/install.sh | sh - 验证安装:
uv --version
必需的API密钥
您将需要来自以下提供程序之一的API密钥:
选项1:谷歌双子座(主要-本指南使用此选项)
- 从以下位置获取API密钥: 谷歌AI工作室
- 免费套餐可用
- 使用的型号:
gemini-2.0-flash-001
选项2:拟人克劳德(备选)
- 从以下位置获取API密钥: 拟人控制台
- 需要付款设置
- 型号选项:
claude-3-5-sonnet-20241022,claude-3-opus-20240229
快速入门(适用于经验丰富的开发人员)
如果你已经熟悉Python开发,只想快速运行:
# Create project
uv init mcp-client
cd mcp-client
# Install dependencies
uv add mcp google-genai python-dotenv
# Create .env file with your API key
echo "GEMINI_API_KEY=your_key_here" > .env
# Remove boilerplate
rm hello.py
# Create the client (copy code from this repo's client.py)
# Then run it
uv run client.py path/to/your/mcp-server.py
对于其他人,请继续下面的详细设置! 👇
从头开始详细设置
让我们一步一步地构建这个,假设你什么都没有开始。
步骤1:创建项目目录
打开终端并运行:
uv init mcp-client
cd mcp-client
这有什么作用:
uv init创建一个具有适当结构的新Python项目- 它会自动创建
pyproject.toml(依赖配置) - 它生成一个样本
hello.py文件(我们将删除此文件)
步骤2:设置虚拟环境
uv venv
这有什么作用:
- 创建一个
.venv包含独立Python环境的目录 - 这使您的项目依赖关系与系统Python分开
- 注: 随着
uv,您通常不需要手动激活venv!uv run自动处理它。
如果要手动激活:
- 窗户:
.venv\Scripts\activate - macOS/Linux:
source .venv/bin/activate
步骤3:安装依赖项
uv add mcp google-genai python-dotenv
这将安装什么:
| 包装 | 用途 | 版本 |
|---|---|---|
mcp |
带CLI工具的MCP SDK,用于连接服务器 | >=1.23.1 |
google-genai |
谷歌用于LLM交互的Gemini AI SDK | >=1.52.0 |
python-dotenv |
从.env文件加载环境变量 | >=1.2.1 |
Claude用户的替代方案:
uv add mcp anthropic python-dotenv
步骤4:删除Boilerplate文件
# Windows
del hello.py
# macOS/Linux
rm hello.py
步骤5:创建环境变量文件
# Windows
echo. > .env
# macOS/Linux
touch .env
现在打开 .env 在文本编辑器中添加:
# For Gemini (Primary)
GEMINI_API_KEY=your_actual_api_key_here
# OR for Claude (Alternative)
# ANTHROPIC_API_KEY=your_actual_api_key_here
重要提示: 替换 your_actual_api_key_here 使用您真正的API密钥!
步骤6:配置Git忽略
echo ".env" >> .gitignore
为什么? 这样可以防止意外地将API密钥提交到版本控制。
你的 .gitignore 还应包括(通常已经存在):
.venv/
__pycache__/
*.pyc
步骤7:创建客户端文件
# Windows
type nul > client.py
# macOS/Linux
touch client.py
现在,您已经准备好添加代码了!继续阅读下一节,了解内容 client.py.
代码架构概述(面向高级开发人员)
在我们逐行解释之前,对于那些想快速了解全局的人来说,这里有一个关于架构的高层次概述。
阶级结构
MCPClient
├── __init__() # Initialize LLM client, setup conversation state
├── connect_to_server() # Establish stdio connection to MCP server
├── process_query() # Main query processing loop with tool calling
├── _execute_tool_call() # Execute single MCP tool call
├── _extract_tool_result() # Parse tool results from various formats
├── chat_loop() # Interactive CLI interface
└── cleanup() # Resource cleanup
关键功能说明
1. __init__(self) -初始化
目的: 设置LLM客户端并初始化对话状态。
关键操作:
- 从环境加载API密钥
- 初始化LLM客户端(Gemini/Claude)
- 创建对话历史记录列表
- 设置异步资源管理器(
AsyncExitStack)
为什么重要: 正确的初始化可以防止运行时错误并设置状态管理。
2. connect_to_server(server_script_path: str) -MCP服务器连接
目的: 建立与MCP服务器的基于stdio的连接,并发现可用的工具。
关键操作:
- 检测服务器类型(带uv的Python、纯Python或Node.js)
- 配置stdio子流程参数
- 初始化MCP客户端会话
- 获取并转换工具定义
为什么重要: MCP使用stdio(标准输入/输出)进行通信。此函数处理不同服务器类型和环境的复杂性。
3. process_query(query: str) -查询处理引擎
目的: 核心对话循环,处理用户输入、调用工具并生成响应。
流量:
User Query → LLM → [Tool Call?] → Execute Tool → LLM → Response
↓
[Multiple tool calls possible]
↓
[Loop until no more tools needed]
关键操作:
- 将用户消息添加到对话历史记录中
- 使用可用工具发送给LLM
- 检测并执行工具调用
- 循环直到最终文本响应
- 更新对话历史记录
为什么重要: 这实现了“代理循环”,使人工智能真正有用——它可以自主使用工具来完成任务。
4. _execute_tool_call(tool_name: str, tool_args: dict) -工具执行
目的: 通过MCP服务器执行单个工具调用并格式化结果。
关键操作:
- 呼叫
session.call_tool()带有工具名称和参数 - 使用提取结果
_extract_tool_result() - 优雅地处理错误
- 格式化LLM消费的结果
为什么重要: 弥合LLM请求和实际工具执行之间的差距。
5. _extract_tool_result(result) -结果解析
目的: 解析MCP工具的结果,这些结果可以有各种格式(文本内容、字典、列表、JSON字符串)。
手柄:
- 内容块列表
- JSON编码字符串
- 纯文本
- 骰子对象
- 多个内容部分
为什么重要: MCP服务器可以返回不同格式的数据。此函数将所有内容规范化为LLM可以理解的一致结构。
6. chat_loop() -交互式界面
目的: 提供一个简单的CLI,用于测试和与客户端交互。
特征:
- 在循环中读取用户输入
- 处理每个查询
- 显示响应
- 支持命令(
quit,clear)
为什么重要: 对测试和调试至关重要。简单但有效的用户界面。
工具函数
convert_mcp_tools_to_gemini(mcp_tools) -工具格式转换
目的: 将MCP工具定义(JSON模式)转换为Gemini的FunctionDeclaration格式。
为什么需要: 每个LLM提供程序都有自己的工具/函数调用模式。这将MCP的标准格式转换为Gemini所期望的格式。
clean_schema(schema) -架构清理
目的: 移除 title JSON模式定义中的字段。
为什么需要: 一些法学硕士提供者窒息 title 模式中的字段。这会递归地删除它们,同时保留实际的模式结构。
数据流图
┌─────────────────────────────────────────────────────────────┐
│ User Input │
└────────────┬────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ MCPClient.process_query() │
│ - Add to conversation history │
│ - Format for LLM │
└────────────┬────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ LLM (Gemini/Claude) │
│ - Analyze query with available tools │
│ - Decide: respond directly OR call tools │
└────────────┬────────────────────────────────────────────────┘
│
┌────┴────┐
│ │
▼ ▼
Text Response Tool Call(s)
│ │
│ ▼
│ ┌─────────────────────────────────────┐
│ │ _execute_tool_call() │
│ │ - Call MCP server via stdio │
│ └────────────┬────────────────────────┘
│ │
│ ▼
│ ┌─────────────────────────────────────┐
│ │ _extract_tool_result() │
│ │ - Parse various formats │
│ │ - Normalize to dict │
│ └────────────┬────────────────────────┘
│ │
│ ▼
│ ┌─────────────────────────────────────┐
│ │ Send tool result back to LLM │
│ └────────────┬────────────────────────┘
│ │
│ ▼
│ [LLM processes result]
│ │
└─────────────────┴──────► Final Response
│
▼
┌──────────────────────┐
│ Display to User │
└──────────────────────┘
详细的代码演练(初学者逐行)
现在让我们构建完整的 client.py 文件,边走边解释每一行。
第1节:进口以及我们为什么需要进口
# Import necessary libraries
import asyncio # For handling asynchronous operations
import os # For environment variable access
import sys # For system-specific parameters and functions
import json # For handling JSON data (used when printing function declarations)
分解它:
-
asyncio:Python用于异步编程的内置库。MCP使用stdio(管道)进行通信,这在异步代码中效果最佳。将异步视为“非阻塞”——在等待MCP服务器响应的同时,我们的程序可以做其他事情。 -
os:提供对操作系统功能的访问。我们使用它来读取环境变量(如API键)和检查文件路径。 -
sys:使我们能够访问系统特定的参数。我们使用sys.argv获取命令行参数(MCP服务器的路径)。 -
json:处理JSON(JavaScript对象表示法)数据。MCP工具使用JSON作为参数和响应,因此我们需要它进行解析。
# Import MCP client components
from typing import Optional # For type hinting optional values
from contextlib import AsyncExitStack # For managing multiple async tasks
from mcp import ClientSession, StdioServerParameters # MCP session management
from mcp.client.stdio import stdio_client # MCP client for standard I/O communication
分解它:
-
Optional:一个类型提示,表示“此变量可以是特定类型或无”。例如,Optional[ClientSession]表示“ClientSession对象或无”。这有助于及早发现错误。 -
AsyncExitStack:一个上下文管理器,帮助我们在完成时清理异步资源。把它想象成离开建筑物时确保所有门都关上,灯都关上。 -
ClientSession:处理与MCP服务器通信的主MCP客户端类。 -
StdioServerParameters:配置如何通过标准输入/输出启动MCP服务器并与之通信(如将数据传输到子流程或从子流程传输数据)。 -
stdio_client:使用stdio实际创建与MCP服务器连接的函数。
# Import Google's Gen AI SDK
from google import genai
from google.genai import types
from google.genai.types import Tool, FunctionDeclaration
from google.genai.types import GenerateContentConfig
from dotenv import load_dotenv # For loading API keys from a .env file
分解它:
-
google.genai:谷歌Gemini AI模型的官方SDK。这就是我们与双子座沟通的方式。 -
types:包含Gemini使用的所有数据类型(如Content、Part等)。 -
Tool和FunctionDeclaration:用于定义Gemini可以调用的工具/函数的类。我们将MCP工具转换为这种格式。 -
GenerateContentConfig:配置对象,用于自定义Gemini生成响应的方式(包括可用的工具)。 -
dotenv:从加载环境变量的库.env文件。这使我们的API密钥保持安全并与代码分离。
# Load environment variables from .env file
load_dotenv()
这有什么作用:
- 阅读
.env项目目录中的文件 - 加载所有变量(如
GEMINI_API_KEY=abc123)进入os.environ - 现在我们可以通过以下方式访问它们
os.getenv("GEMINI_API_KEY")
我们为什么这样做:
- 保守代码秘密(永远不要将API密钥提交到Git!)
- 使在不同环境中使用不同密钥变得容易
第2节:MCPClient类-初始化
class MCPClient:
def __init__(self):
"""Initialize the MCP client and configure the Gemini API."""
self.session: Optional[ClientSession] = None # MCP session for communication
self.exit_stack = AsyncExitStack() # Manages async resource cleanup
self.conversation_history = [] # Maintain conversation history
分解它:
-
class MCPClient::为我们的MCP客户端定义一个类(蓝图)。类将一起工作的数据和函数捆绑在一起。 -
def __init__(self)::当您创建新的MCPClient实例时,构造函数会自动运行。它建立了初始状态。 -
self.session: Optional[ClientSession] = None:self.表示该变量属于该实例- 开始为
None,将在我们连接到服务器时设置 - 存储我们的活动MCP会话
-
self.exit_stack = AsyncExitStack():- 创建用于异步清理的资源管理器
- 把它看作是一个“清理清单”,确保我们正确地关闭连接
-
self.conversation_history = []:- 用于存储对话的空列表
- 每个条目都是一条消息(用户或助手)
- 这给出了前几轮的人工智能背景
# Retrieve the Gemini API key from environment variables
gemini_api_key = os.getenv("GEMINI_API_KEY")
if not gemini_api_key:
raise ValueError("GEMINI_API_KEY not found. Please add it to your .env file.")
分解它:
-
os.getenv("GEMINI_API_KEY"):- 查找名为的环境变量
GEMINI_API_KEY - 如果找到,则返回值,或
None如果未找到 - 这来自我们的
.env文件(通过load_dotenv())
- 查找名为的环境变量
-
if not gemini_api_key::- 检查密钥是否丢失(无或空字符串)
- 如果是这样,则引发错误以提醒用户
-
raise ValueError(...):- 立即停止程序并显示错误消息
- 这是一种“快速失败”的方法——与其稍后神秘地失败,不如尽早出现明显的错误
为什么这很重要: LLM访问需要API密钥。及早检查可以防止浪费时间和混淆错误。
# Configure the Gemini AI client
self.genai_client = genai.Client(api_key=gemini_api_key)
分解它:
genai.Client(api_key=...):创建Gemini API客户端self.genai_client =:将其存储为实例变量,以便所有方法都可以使用它- 此客户端处理与Gemini服务器的所有通信
第3节:连接到MCP服务器
async def connect_to_server(self, server_script_path: str):
"""Connect to the MCP server and list available tools."""
# Determine whether the server script is written in Python or JavaScript
if server_script_path.endswith('.py'):
分解它:
async def:定义异步函数(可以使用await内部)server_script_path: str:需要字符串的参数-MCP服务器脚本的路径if server_script_path.endswith('.py')::检查文件是否为Python(vs Node.js)
# Use uv run to ensure the server runs in its own virtual environment
server_dir = os.path.dirname(os.path.abspath(server_script_path))
script_name = os.path.basename(server_script_path)
分解它:
-
os.path.abspath(server_script_path):将相对路径转换为绝对(完整)路径- 例子:
../server.py→/Users/you/projects/server.py
- 例子:
-
os.path.dirname(...):获取路径的目录部分- 例子:
/Users/you/projects/server.py→/Users/you/projects
- 例子:
-
os.path.basename(...):仅获取文件名- 例子:
/Users/you/projects/server.py→server.py
- 例子:
我们为什么这样做: 我们需要从服务器自己的目录运行服务器(这样它就可以找到它的依赖关系),但我们可能会收到一个相对路径。
# Check if server directory has uv project files
if os.path.exists(os.path.join(server_dir, 'uv.lock')) or os.path.exists(os.path.join(server_dir, 'pyproject.toml')):
command = "uv"
# Use --directory to run from the server's directory
args = ["run", "--directory", server_dir, "python", script_name]
else:
command = "python"
args = [server_script_path]
分解它:
-
os.path.join(server_dir, 'uv.lock'):安全地组合路径部分- 例子:
("/Users/you/projects", "uv.lock")→/Users/you/projects/uv.lock - 跨平台(适用于Windows、Mac、Linux)
- 例子:
-
os.path.exists(...):检查文件或目录是否存在 -
逻辑:
- 如果服务器使用
uv(有uv.lock或pyproject.toml),使用uv run - 否则,请使用常规
python命令
- 如果服务器使用
-
为什么?
uv run自动处理服务器的虚拟环境。这可以防止依赖冲突。
else:
command = "node"
args = [server_script_path]
分解它:
- 如果服务器文件没有以结尾
.py,假设它是一个Node.js服务器 - 用它运行
node命令
# Define the parameters for connecting to the MCP server
server_params = StdioServerParameters(command=command, args=args)
分解它:
StdioServerParameters:告诉MCP如何启动服务器的配置对象command=command:要运行的可执行文件(uv,python,或node)args=args:传递给该命令的参数
结果示例:
- 对于uv Python服务器:
uv run --directory /path/to/server python server.py - 对于普通Python服务器:
python /path/to/server.py - 对于Node.js服务器:
node /path/to/server.js
# Establish communication with the MCP server using standard input/output (stdio)
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
分解它:
-
stdio_client(server_params):创建异步上下文管理器,该管理器:- 将MCP服务器作为子进程启动
- 连接到其stdin/stdout
- 返回读/写流
-
await self.exit_stack.enter_async_context(...):- 在我们的清理堆栈中注册连接
- 当我们打电话
cleanup()稍后,这将确保子流程正确终止 await表示“等待此异步操作完成”
-
stdio_transport =:存储传输对象(包含读/写流)
# Extract the read/write streams from the transport object
self.stdio, self.write = stdio_transport
分解它:
- 元组拆包:
stdio_transport包含两个值(读流、写流) self.stdio:从MCP服务器读取的流self.write:用于写入MCP服务器的流- 这些是双向通信的“管道”
# Initialize the MCP client session, which allows interaction with the server
self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
分解它:
ClientSession(self.stdio, self.write):使用我们的流创建MCP会话- 本课程提供了以下高级方法
call_tool(),list_tools()等等。 - 再次注册
exit_stack进行适当的清理
# Send an initialization request to the MCP server
await self.session.initialize()
分解它:
await self.session.initialize():发送MCP初始化握手- 这告诉服务器我们准备好了,并协商协议细节
- 必须在任何其他操作之前完成
# Request the list of available tools from the MCP server
response = await self.session.list_tools()
tools = response.tools # Extract the tool list from the response
# Print a message showing the names of the tools available on the server
print("\nConnected to server with tools:", [tool.name for tool in tools])
分解它:
await self.session.list_tools():问服务器“你们提供什么工具?”- 返回一个响应对象
tools属性 [tool.name for tool in tools]:列表理解-仅创建工具名称列表- 打印确认信息,以便用户知道连接成功
# Convert MCP tools to Gemini format
self.function_declarations = convert_mcp_tools_to_gemini(tools)
分解它:
- 调用我们的辅助函数(稍后定义)来转换工具
- MCP工具使用JSON模式格式
- 双子座需要
FunctionDeclaration格式 - 这种转换使Gemini可以使用这些工具
第4节:提取工具结果
这是一个比较棘手的部分,因为MCP服务器可以以许多不同的格式返回数据。
def _extract_tool_result(self, result):
"""Extract content from MCP tool result, handling different formats."""
if not result or not hasattr(result, 'content'):
return None
分解它:
_extract_tool_result:下划线前缀是Python惯例,表示“私有方法”(仅供内部使用)if not result:如果结果为None或空,则返回Nonehasattr(result, 'content'):检查结果是否有content属性- 如果结果格式不正确,则通过返回None来优雅地失败
content = result.content
# Handle list of content blocks - collect ALL content, not just first
if isinstance(content, list) and len(content) > 0:
extracted_parts = []
分解它:
result.content:工具的实际数据isinstance(content, list):检查内容是否为列表extracted_parts = []:将存储我们提取的所有内容
为什么是列表? 一些工具返回多个内容块(例如,多个文件、多个结果)。
for content_block in content:
# Check if it's a TextContent object with text attribute
if hasattr(content_block, 'text'):
text = content_block.text
# Try to parse as JSON if it looks like JSON
try:
parsed = json.loads(text)
extracted_parts.append(parsed)
except (json.JSONDecodeError, TypeError):
extracted_parts.append(text)
分解它:
- 循环浏览每个内容块
hasattr(content_block, 'text'):检查它是否是TextContent对象json.loads(text):尝试将文本解析为JSON- 例子:
'{"name":"John"}'→{"name":"John"}字典
- 例子:
except (json.JSONDecodeError, TypeError)::如果它不是有效的JSON,请保持为纯文本- 添加到
extracted_parts列表
为什么要尝试/排除? 有些文本看起来像JSON,但实际上不是,有些工具返回需要解析的JSON字符串。
# Check if it's a dict with text key
elif isinstance(content_block, dict):
if 'text' in content_block:
text = content_block['text']
# Try to parse as JSON if it looks like JSON
try:
parsed = json.loads(text)
extracted_parts.append(parsed)
except (json.JSONDecodeError, TypeError):
extracted_parts.append(text)
else:
# If it's a dict without 'text', it might be the data itself
extracted_parts.append(content_block)
分解它:
- **
elif isinstance(content_block, dict):**如果它已经是一本词典 if 'text' in content_block::检查它是否有“文本”键- 如果是,提取并尝试解析它
else::如果这是一个普通的字典,它可能是实际的数据——按原样使用
例子:
{"text": "{'result': 5}"}→ 尝试解析文本{"result": 5}→ 直接使用
# Check if it's already a string
elif isinstance(content_block, str):
# Try to parse as JSON
try:
parsed = json.loads(content_block)
extracted_parts.append(parsed)
except (json.JSONDecodeError, TypeError):
extracted_parts.append(content_block)
# Try to convert to string
else:
extracted_parts.append(str(content_block))
分解它:
elif isinstance(content_block, str)::如果是纯字符串,请尝试解析为JSONelse::最后手段-将任何内容转换为字符串
这可以处理边缘情况 如数值结果、布尔结果等。
# If we have multiple parts, combine them appropriately
if len(extracted_parts) == 1:
return extracted_parts[0]
else:
# If all parts are lists, flatten them
if all(isinstance(p, list) for p in extracted_parts):
flattened = []
for p in extracted_parts:
flattened.extend(p)
return flattened
return extracted_parts
分解它:
if len(extracted_parts) == 1::如果只有一个结果,则直接返回(不包含在列表中)all(isinstance(p, list) for p in extracted_parts):检查所有零件是否都是清单- 如果是这样,请将其压平:
[[1,2], [3,4]]→[1,2,3,4]
- 如果是这样,请将其压平:
flattened.extend(p):将从p到flat的所有元素相加- 否则,按原样返回零件列表
# Handle direct content (string or dict)
elif isinstance(content, str):
# Try to parse as JSON
try:
return json.loads(content)
except (json.JSONDecodeError, TypeError):
return content
elif isinstance(content, dict):
return content
else:
# Fallback: convert to string
return str(content) if content else None
分解它:
- 这可以处理以下情况
content不是列表 - 直接字符串→ 尝试解析为JSON
- 直接口述→ 按原样返回
- 还有别的吗→ 转换为字符串
为什么这么复杂? 不同的MCP服务器对响应的格式不同。此函数将所有格式标准化为一致的格式。
第5节:执行工具调用
async def _execute_tool_call(self, tool_name: str, tool_args: dict):
"""Execute a tool call and return the result in a format suitable for Gemini."""
try:
result = await self.session.call_tool(tool_name, tool_args)
extracted_result = self._extract_tool_result(result)
分解它:
async def:异步,因为工具执行可能需要时间tool_name: str:要调用的工具的名称(例如,“read_file”)tool_args: dict:工具的论据(例如。,{"path": "/path/to/file"})await self.session.call_tool(...):将工具调用请求发送到MCP服务器extracted_result = self._extract_tool_result(result):分析原始结果
# If the result is a string that looks like JSON, try to parse it
if isinstance(extracted_result, str):
try:
parsed = json.loads(extracted_result)
return {"result": parsed}
except (json.JSONDecodeError, TypeError):
# Not JSON, return as string
return {"result": extracted_result}
return {"result": extracted_result}
分解它:
- 如果我们得到一个字符串,请再次尝试解析JSON
- 始终返回带有“result”键的字典
- 为什么?Gemini希望工具结果格式一致
- 例子:
"hello"→{"result": "hello"} - 例子:
'{"count": 5}'→{"result": {"count": 5}}
except Exception as e:
print(f"[Error executing tool {tool_name}: {e}]")
return {"error": str(e), "result": None}
分解它:
except Exception as e::捕获任何错误- 将错误消息打印到控制台
- 返回一个错误字典,以便LLM知道出了什么问题
- 程序继续运行(不会崩溃)
为什么? 工具故障不应导致整个客户端崩溃。LLM可以优雅地处理错误。
第6节:处理用户查询(主循环)
这是客户的核心——魔法发生的地方!
async def process_query(self, query: str) -> str:
"""
Process a user query using the Gemini API and execute tool calls if needed.
Args:
query (str): The user's input query.
Returns:
str: The response generated by the Gemini model.
"""
# Format user input as a structured Content object for Gemini
user_prompt_content = types.Content(
role='user',
parts=[types.Part.from_text(text=query)]
)
分解它:
-
async def process_query(self, query: str) -> str::- 将用户的问题视为字符串
- 以字符串形式返回AI的响应
-> str是返回值的类型提示
-
types.Content:Gemini的消息格式role='user':此消息来自用户parts=[...]:消息部分列表(可以是文本、图像等)types.Part.from_text(text=query):从查询中创建文本部分
例子: 如果查询是“什么是2+2?”,则创建:
Content(role='user', parts=[Part(text="What's 2+2?")])
# Add user message to conversation history
self.conversation_history.append(user_prompt_content)
分解它:
- 将用户的消息添加到我们的历史记录列表中
- 这可以在多个回合中保持上下文
- AI可以参考早期的消息
# Send user input to Gemini AI and include available tools for function calling
response = self.genai_client.models.generate_content(
model='gemini-2.0-flash-001',
contents=self.conversation_history,
config=types.GenerateContentConfig(
tools=self.function_declarations,
),
)
分解它:
self.genai_client.models.generate_content(...):调用Gemini APImodel='gemini-2.0-flash-001':指定要使用的模型2.0-flash快速且经济高效- 你可以使用
gemini-2.0-pro-001用于更复杂的推理
contents=self.conversation_history:发送完整的对话以了解上下文tools=self.function_declarations:告诉Gemini有哪些可用的工具
发生了什么: Gemini分析对话并决定是否:
- 直接用文本回复,或者
- 先调用一个或多个工具
# Initialize variables to store final response text
final_text = []
max_iterations = 10 # Prevent infinite loops
iteration = 0
# Process the response - handle multiple function calls and follow-up responses
while iteration < max_iterations:
iteration += 1
function_calls_made = False
分解它:
final_text = []:将收集所有文本回复max_iterations = 10:防止无限工具调用循环的安全限制function_calls_made = False:跟踪是否有任何工具被称为此迭代
为什么是循环? AI可能:
- 调用工具
- 获取结果
- 根据该结果调用另一个工具
- 得到那个结果
- 最后用文本回复
这个循环处理整个链条。
# Process all candidates in the response
for candidate in response.candidates:
if candidate.content.parts:
for part in candidate.content.parts:
if isinstance(part, types.Part):
if part.function_call:
分解它:
- **
response.candidates**双子座可以返回多个候选答案(通常只有一个) candidate.content.parts:每个候选人的内容都分为几个部分if part.function_call::检查此部分是否为工具调用(与文本相比)
结构:
Response
└── Candidates [list]
└── Candidate
└── Content
└── Parts [list]
├── Part (text)
└── Part (function_call)
# Extract function call details
function_call_part = part
tool_name = function_call_part.function_call.name
tool_args = function_call_part.function_call.args
# Print debug info
print(f"\n[Gemini requested tool call: {tool_name} with args {tool_args}]")
分解它:
function_call_part.function_call.name:要调用的工具的名称function_call_part.function_call.args:论点(作为格言)- 打印信息,以便用户看到正在发生的事情
输出示例:
[Gemini requested tool call: read_file with args {'path': '/home/user/data.txt'}]
# Execute the tool using the MCP server
function_response = await self._execute_tool_call(tool_name, tool_args)
分解它:
- 呼叫我们
_execute_tool_call方法 - 这会将请求发送到MCP服务器
- 返回工具的结果
# Format the tool response for Gemini
function_response_part = types.Part.from_function_response(
name=tool_name,
response=function_response
)
# Structure the tool response as a Content object for Gemini
function_response_content = types.Content(
role='tool',
parts=[function_response_part]
)
分解它:
types.Part.from_function_response(...):创建函数响应部分- 包括工具名称和结果
types.Content(role='tool', ...):将其包装在Content对象中role='tool'表示此消息来自工具执行
# Add function call and response to conversation history
self.conversation_history.append(
types.Content(role='model', parts=[function_call_part])
)
self.conversation_history.append(function_response_content)
function_calls_made = True
分解它:
- 将两条消息添加到历史记录中:
- AI的函数调用请求(
role='model') - 工具的响应(
role='tool')
- AI的函数调用请求(
- 集合
function_calls_made = True继续循环 - 这保持了完整的对话上下文
历史示例:
[user] "What files are in my directory?"
[model] <calls list_files tool>
[tool] ["file1.txt", "file2.py"]
[model] "Your directory contains file1.txt and file2.py"
else:
# Direct text response (no function call)
if hasattr(part, 'text') and part.text:
final_text.append(part.text)
分解它:
- 如果该部分不是函数调用,则必须是文本
hasattr(part, 'text'):确保它具有文本属性- 将文本添加到我们的
final_text列表
# If no function calls were made, we're done
if not function_calls_made:
break
分解它:
- 如果人工智能没有调用任何工具,我们就有了最终的回应
- 跳出循环
# Get follow-up response from Gemini after tool execution
response = self.genai_client.models.generate_content(
model='gemini-2.0-flash-001',
contents=self.conversation_history,
config=types.GenerateContentConfig(
tools=self.function_declarations,
),
)
分解它:
- 再次致电Gemini,更新历史记录(现在包括工具结果)
- 双子座可以:
- 调用更多工具(循环继续)
- 用最终文本进行响应(循环结束)
这就是“代理循环” -AI可以自主地链接多个工具调用。
# Add final response to conversation history
if final_text:
response_text = "\n".join(final_text)
self.conversation_history.append(
types.Content(
role='model',
parts=[types.Part.from_text(text=response_text)]
)
)
# Return the combined response as a single formatted string
return "\n".join(final_text) if final_text else "No response generated."
分解它:
"\n".join(final_text):将所有文本部分与换行符组合- 例子:
["Hello", "World"]→"Hello\nWorld"
- 例子:
- 将最终响应添加到历史记录中
- 返回要显示给用户的组合文本
第7节:辅助方法
def clear_history(self):
"""Clear the conversation history."""
self.conversation_history = []
分解它:
- 重置对话的简单助手
- 可用于在不重新启动程序的情况下重新启动
async def chat_loop(self):
"""Run an interactive chat session with the user."""
print("\nMCP Client Started! Type 'quit' to exit, 'clear' to clear history.")
while True:
query = input("\nQuery: ").strip()
if query.lower() == 'quit':
break
if query.lower() == 'clear':
self.clear_history()
print("Conversation history cleared.")
continue
# Process the user's query and display the response
response = await self.process_query(query)
print("\n" + response)
分解它:
while True::无限循环(直到用户键入“quit”)input("\nQuery: "):等待用户输入.strip():删除前导/尾随空格if query.lower() == 'quit'::检查退出命令(不区分大小写)break:退出循环if query.lower() == 'clear'::检查命令是否清晰continue:跳到下一次迭代而不进行处理- 否则,处理查询并打印响应
async def cleanup(self):
"""Clean up resources before exiting."""
await self.exit_stack.aclose()
分解它:
await self.exit_stack.aclose():关闭所有已注册的异步上下文- 这个:
- 关闭MCP会话
- 关闭stdio流
- 终止MCP服务器子进程
- 清洁停机必不可少
第8节:实用功能
def clean_schema(schema):
"""
Recursively removes 'title' fields from the JSON schema.
Args:
schema (dict): The schema dictionary.
Returns:
dict: Cleaned schema without 'title' fields.
"""
if isinstance(schema, dict):
schema.pop("title", None) # Remove title if present
# Recursively clean nested properties
if "properties" in schema and isinstance(schema["properties"], dict):
for key in schema["properties"]:
schema["properties"][key] = clean_schema(schema["properties"][key])
return schema
分解它:
schema.pop("title", None):删除“title”键(如果存在)pop如果键丢失,第二个参数不会引发错误
- 递归:如果架构具有嵌套属性,请也清理这些属性
- 为什么? 一些LLM(包括Gemini)在JSON模式中的“title”字段存在问题
例子:
# Before
{"type": "object", "title": "MyObject", "properties": {"name": {"type": "string", "title": "Name"}}}
# After
{"type": "object", "properties": {"name": {"type": "string"}}}
def convert_mcp_tools_to_gemini(mcp_tools):
"""
Converts MCP tool definitions to the correct format for Gemini API function calling.
Args:
mcp_tools (list): List of MCP tool objects with 'name', 'description', and 'inputSchema'.
Returns:
list: List of Gemini Tool objects with properly formatted function declarations.
"""
gemini_tools = []
for tool in mcp_tools:
# Ensure inputSchema is a valid JSON schema and clean it
parameters = clean_schema(tool.inputSchema.copy() if hasattr(tool.inputSchema, 'copy') else tool.inputSchema)
分解它:
- 循环每个MCP工具
tool.inputSchema:定义工具参数的JSON模式.copy():创建副本,这样我们就不会修改原始副本clean_schema(...):删除有问题的“标题”字段
# Construct the function declaration
function_declaration = FunctionDeclaration(
name=tool.name,
description=tool.description,
parameters=parameters
)
# Wrap in a Tool object
gemini_tool = Tool(function_declarations=[function_declaration])
gemini_tools.append(gemini_tool)
return gemini_tools
分解它:
FunctionDeclaration:Gemini定义可调用函数的格式- 名称:怎么称呼它
- 描述:它做什么(帮助AI决定何时使用它)
- 参数:接受输入的JSON模式
Tool(function_declarations=[...]):包装函数声明- 将所有工具收集到列表中并返回
格式转换:
MCP Tool → FunctionDeclaration → Tool → List of Tools
第9节:主要入口点
async def main():
"""Main function to start the MCP client."""
if len(sys.argv) < 2:
print("Usage: python client.py <path_to_server_script>")
sys.exit(1)
分解它:
sys.argv:命令行参数列表sys.argv[0]是脚本名称(client.py)sys.argv[1]应该是服务器路径
if len(sys.argv) < 2::检查是否提供了服务器路径sys.exit(1):退出时出现错误代码1(表示失败)
client = MCPClient()
try:
# Connect to the MCP server and start the chat loop
await client.connect_to_server(sys.argv[1])
await client.chat_loop()
finally:
# Ensure resources are cleaned up
await client.cleanup()
分解它:
client = MCPClient():创建客户端的实例try::启动一个try块await client.connect_to_server(sys.argv[1]):使用提供的路径连接await client.chat_loop():启动交互循环finally::此块始终运行,即使出现错误或用户退出await client.cleanup():清理资源
为什么要尝试/最终? 确保即使出现问题也能进行清理。
if __name__ == "__main__":
# Run the main function within the asyncio event loop
asyncio.run(main())
分解它:
if __name__ == "__main__"::仅在直接执行此文件(未导入)时运行asyncio.run(main()):启动异步事件循环并运行main()- 这是所有异步代码的入口点
使用不同的LLM提供者
当前实施:Google Gemini
为什么是双子座?
此实现使用谷歌的Gemini有几个原因:
- 免费层:为开发和测试提供充足的免费配额
- 最新功能:Gemini 2.0具有出色的函数调用支持
- 快速响应:Flash型号针对速度进行了优化
- 简单SDK:谷歌
google-genai该包易于使用 - 良好的文件:清晰的示例和API参考
模型选择
model='gemini-2.0-flash-001'
可用型号:
gemini-2.0-flash-001:快速、高效,非常适合大多数任务gemini-2.0-pro-001:对于复杂的任务,推理能力更强,速度较慢gemini-1.5-flash:上一代,仍然很有能力gemini-1.5-pro:上一代专业车型
API设置
Gemini设置非常简单:
from google import genai
gemini_api_key = os.getenv("GEMINI_API_KEY")
self.genai_client = genai.Client(api_key=gemini_api_key)
消息格式
Gemini使用 Content 和 Part 结构:
types.Content(
role='user', # or 'model' or 'tool'
parts=[
types.Part.from_text(text="Hello"),
# Can also include images, function calls, etc.
]
)
替代品:拟人克劳德
如果你更喜欢使用Claude而不是Gemini,以下是如何调整代码。
步骤1:更改依赖关系
在您的终端中:
# Remove Gemini
uv remove google-genai
# Add Claude
uv add anthropic
步骤2:更新环境变量
在你的 .env 文件:
# Change from:
# GEMINI_API_KEY=your_key_here
# To:
ANTHROPIC_API_KEY=your_key_here
步骤3:修改导入
替换Gemini导入:
# OLD (Gemini)
from google import genai
from google.genai import types
from google.genai.types import Tool, FunctionDeclaration
from google.genai.types import GenerateContentConfig
# NEW (Claude)
from anthropic import Anthropic
步骤4:更新初始化
更换 __init__ 方法:
def __init__(self):
"""Initialize the MCP client and configure the Claude API."""
self.session: Optional[ClientSession] = None
self.exit_stack = AsyncExitStack()
self.conversation_history = [] # Format: [{"role": "user", "content": "..."}, ...]
# Retrieve the Anthropic API key
anthropic_api_key = os.getenv("ANTHROPIC_API_KEY")
if not anthropic_api_key:
raise ValueError("ANTHROPIC_API_KEY not found. Please add it to your .env file.")
# Configure the Anthropic client
self.anthropic_client = Anthropic(api_key=anthropic_api_key)
步骤5:调整工具格式转换
Claude使用了不同的工具格式。替换 convert_mcp_tools_to_gemini:
def convert_mcp_tools_to_claude(mcp_tools):
"""
Converts MCP tool definitions to Claude's tool format.
Args:
mcp_tools (list): List of MCP tool objects.
Returns:
list: List of tool definitions in Claude format.
"""
claude_tools = []
for tool in mcp_tools:
parameters = clean_schema(
tool.inputSchema.copy() if hasattr(tool.inputSchema, 'copy') else tool.inputSchema
)
claude_tool = {
"name": tool.name,
"description": tool.description,
"input_schema": parameters
}
claude_tools.append(claude_tool)
return claude_tools
步骤6:更新 connect_to_server
更改工具转换行:
# OLD (Gemini)
self.function_declarations = convert_mcp_tools_to_gemini(tools)
# NEW (Claude)
self.claude_tools = convert_mcp_tools_to_claude(tools)
步骤7:完全重写 process_query
克劳德的API不同。以下是完整的替换:
async def process_query(self, query: str) -> str:
"""
Process a user query using Claude API and execute tool calls if needed.
Args:
query (str): The user's input query.
Returns:
str: The response generated by Claude.
"""
# Add user message to conversation history
self.conversation_history.append({
"role": "user",
"content": query
})
max_iterations = 10
iteration = 0
while iteration < max_iterations:
iteration += 1
# Call Claude with conversation history and available tools
response = self.anthropic_client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=4096,
messages=self.conversation_history,
tools=self.claude_tools
)
# Add assistant response to history
assistant_message = {
"role": "assistant",
"content": response.content
}
self.conversation_history.append(assistant_message)
# Check if Claude wants to use tools
tool_uses = [block for block in response.content if block.type == "tool_use"]
if not tool_uses:
# No tools needed, extract text and return
text_blocks = [block.text for block in response.content if hasattr(block, 'text')]
return "\n".join(text_blocks) if text_blocks else "No response generated."
# Execute all tool calls
tool_results = []
for tool_use in tool_uses:
print(f"\n[Claude requested tool call: {tool_use.name} with args {tool_use.input}]")
result = await self._execute_tool_call(tool_use.name, tool_use.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": json.dumps(result)
})
# Add tool results as a user message
self.conversation_history.append({
"role": "user",
"content": tool_results
})
return "Maximum iterations reached without final response."
对比表
| 方面 | 双子座 | 克劳德 |
|---|---|---|
| 包裹 | google-genai |
anthropic |
| 客户端初始化 | genai.Client(api_key=...) |
Anthropic(api_key=...) |
| 消息格式 | Content 对象与 Parts |
骰子与 role 和 content |
| 工具格式 | FunctionDeclaration 在 Tool |
骰子与 name, description, input_schema |
| API调用 | client.models.generate_content() |
client.messages.create() |
| 刀具检测 | 检查 part.function_call |
检查 block.type == "tool_use" |
| 工具结果 | Content(role='tool', ...) |
用户消息 tool_result 类型 |
| 免费层 | 是的,慷慨 | 不,需要付款 |
| 流媒体 | 支持 | 支持 |
| 最大令牌数 | 在模型配置中设置 | 显式 max_tokens 参数 |
你应该选择哪一个?
在以下情况下选择Gemini:
- 你想要一个免费的开发选项
- 您需要快速响应
- 你正在构建一个原型或个人项目
如果出现以下情况,请选择克劳德:
- 你需要绝对最好的推理能力
- 您正在构建一个生产应用程序
- 预算不是限制
- 你需要克劳德的特定功能(比如200K上下文窗口)
运行客户端
命令语法
uv run client.py <path_to_server_script>
示例:
# Python MCP server with uv
uv run client.py ../my-mcp-server/server.py
# Python MCP server (plain)
uv run client.py /path/to/server.py
# Node.js MCP server
uv run client.py ./server.js
交互式命令
跑步后,你会看到:
Connected to server with tools: ['read_file', 'write_file', 'list_directory']
MCP Client Started! Type 'quit' to exit, 'clear' to clear history.
Query:
可用命令:
| 命令 | 操作 |
|---|---|
| 任何文本 | 使用AI处理您的查询 |
quit |
退出程序 |
clear |
清除对话历史记录(重新开始) |
示例会话
Query: What files are in my current directory?
[Gemini requested tool call: list_directory with args {'path': '.'}]
Your current directory contains:
- client.py
- pyproject.toml
- .env
- README.md
Query: Read the pyproject.toml file
[Gemini requested tool call: read_file with args {'path': 'pyproject.toml'}]
The pyproject.toml file contains your project configuration:
- Project name: mcp-client
- Python version: >=3.13
- Dependencies: google-genai, mcp, python-dotenv
Query: quit
测试您的设置
1.验证MCP服务器连接
输出良好:
Connected to server with tools: ['tool1', 'tool2', 'tool3']
MCP Client Started! Type 'quit' to exit, 'clear' to clear history.
输出错误:
Error: Could not connect to server
修复: 检查:
- 服务器路径正确
- 服务器文件可执行
- 已安装服务器依赖项
2.测试一个简单的工具调用
尝试一个应该触发工具的查询:
Query: Use the [tool_name] tool to [do something]
输出良好:
[Gemini requested tool call: tool_name with args {...}]
[Tool response shown]
输出错误:
I don't have access to that tool.
修复:
- 确保该工具列在“使用工具连接到服务器”中
- 明确你想要什么
3.测试对话记忆
Query: My favorite color is blue
Response: Got it, I'll remember that your favorite color is blue.
Query: What's my favorite color?
Response: Your favorite color is blue.
如果第二个响应不记得,请检查:
- 你不打电话
clear消息之间 - 这
conversation_history未重置
4.调试提示
启用详细日志记录:
在导入后添加以下内容:
import logging
logging.basicConfig(level=logging.DEBUG)
打印对话历史记录:
添加到 process_query:
print(f"[DEBUG] Conversation has {len(self.conversation_history)} messages")
打印原始工具结果:
在 _extract_tool_result,取消注释:
print(f"[DEBUG] Tool returned: {type(extracted_result)} = {extracted_result}")
故障排除
错误:“找不到GEMINI_API_KEY”
原因: 中未设置API密钥 .env 文件。
修复:
- 创建
.env项目根目录中的文件 - 添加:
GEMINI_API_KEY=your_actual_key - 重新启动客户端
错误:“找不到模块:mcp”或“google”
原因: 未安装依赖项。
修复:
uv add mcp google-genai python-dotenv
错误:“无法连接到服务器”
可能的原因:
- 服务器路径错误
- 服务器有错误
- 缺少服务器依赖项
修复:
# Test server directly
python path/to/server.py
# Or for Node.js
node path/to/server.js
# Check server has dependencies
cd path/to/server
uv sync # for Python with uv
# or npm install for Node.js
错误:“已达到最大迭代次数”
原因: 工具调用循环没有终止(AI继续调用工具)。
可能的原因:
- 工具返回错误的结果
- AI陷入循环
- 工具逻辑中的错误
修复:
- 检查工具结果格式
- 尝试
clear重置对话 - 增加
max_iterations如果需要(但要调查根本原因)
工具未被调用
症状: AI用文本而不是使用工具进行响应。
原因:
- 查询不够具体
- 工具描述不清楚
- 工具架构有错误
修复:
- 明确:“使用read_file工具读取example.txt”
- 检查工具说明是否清晰
- 验证工具模式是否为有效的JSON模式
API速率限制
错误: “429请求太多”或“资源耗尽”
修复:
- 在请求之间等待几秒钟
- 检查您的API配额/限制
- 考虑升级API计划
Windows特定:路径问题
问题: 在Windows上找不到服务器路径。
修复: 使用正斜杠或原始字符串:
# Good
uv run client.py ../server/server.py
# Also good
uv run client.py C:/Users/You/server/server.py
# Or in Python code use raw string
r"C:\Users\You\server\server.py"
项目结构参考
完成所有设置步骤后,您的项目应该如下所示:
mcp-client/
├── .venv/ # Virtual environment (created by uv venv)
│ ├── Scripts/ # (Windows) or bin/ (Unix)
│ ├── Lib/
│ └── ...
├── client.py # Main MCP client code (YOU CREATE THIS)
├── main.py # Boilerplate (can be ignored or deleted)
├── pyproject.toml # Project metadata and dependencies
├── uv.lock # Locked dependency versions
├── .env # API keys (YOU CREATE THIS)
├── .gitignore # Git ignore rules
└── README.md # This documentation
文件描述
| 文件 | 目的 | 创建者 |
|---|---|---|
client.py |
主MCP客户端实现 | 您 |
pyproject.toml |
Python项目配置、依赖关系 | uv init |
uv.lock |
锁定依赖版本以实现可重复性 | uv add |
.env |
环境变量(API键) | 您 |
.venv/ |
独立的Python环境 | uv venv |
.gitignore |
要从版本控制中排除的文件 | uv init |
README.md |
文档(此文件) | 您/已更新 |
重要说明
不要使用Git:
.env(包含秘密!).venv/(太大,很容易重新创建)__pycache__/(Python字节码)
提交到Git:
client.pypyproject.tomluv.lockREADME.md.gitignore
后续步骤和扩展
祝贺您现在拥有一个功能齐全的MCP客户端。以下是扩展它的想法:
1.添加更多MCP服务器
同时连接到多个服务器:
# In __init__
self.sessions = [] # List of sessions
# Create multiple connections
await client.connect_to_server("server1.py")
await client.connect_to_server("server2.js")
# Merge tool lists
all_tools = self.sessions[0].tools + self.sessions[1].tools
2.实现流式响应
逐字获取响应,而不是一次性获取所有响应:
双子座:
response_stream = self.genai_client.models.generate_content_stream(
model='gemini-2.0-flash-001',
contents=self.conversation_history,
config=types.GenerateContentConfig(tools=self.function_declarations),
)
for chunk in response_stream:
if chunk.text:
print(chunk.text, end='', flush=True)
3.构建GUI界面
使用以下图形界面替换CLI:
选项A:Tkinter(内置)
import tkinter as tk
from tkinter import scrolledtext
class MCPClientGUI:
def __init__(self):
self.root = tk.Tk()
self.root.title("MCP Client")
self.chat_display = scrolledtext.ScrolledText(self.root, wrap=tk.WORD)
self.chat_display.pack(expand=True, fill='both')
self.input_field = tk.Entry(self.root)
self.input_field.pack(fill='x')
self.input_field.bind('<Return>', self.send_message)
def send_message(self, event):
query = self.input_field.get()
# Process with MCP client...
选项B:Streamlit(基于网络)
uv add streamlit
import streamlit as st
st.title("MCP Client")
if "messages" not in st.session_state:
st.session_state.messages = []
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
if prompt := st.chat_input("Ask me anything"):
response = asyncio.run(client.process_query(prompt))
st.session_state.messages.append({"role": "assistant", "content": response})
4.高级错误恢复
为瞬态故障添加重试逻辑:
async def _execute_tool_call_with_retry(self, tool_name: str, tool_args: dict, max_retries=3):
"""Execute tool call with automatic retry on failure."""
for attempt in range(max_retries):
try:
return await self._execute_tool_call(tool_name, tool_args)
except Exception as e:
if attempt == max_retries - 1:
raise
print(f"[Retry {attempt + 1}/{max_retries}] Tool call failed: {e}")
await asyncio.sleep(2 ** attempt) # Exponential backoff
5.会话持久性
保存并加载对话历史记录:
import json
from datetime import datetime
def save_conversation(self, filename=None):
"""Save conversation history to a JSON file."""
if filename is None:
filename = f"conversation_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
with open(filename, 'w') as f:
json.dump(self.conversation_history, f, indent=2)
print(f"Conversation saved to {filename}")
def load_conversation(self, filename):
"""Load conversation history from a JSON file."""
with open(filename, 'r') as f:
self.conversation_history = json.load(f)
print(f"Conversation loaded from {filename}")
用途:
# In chat_loop, add new commands:
if query.lower() == 'save':
self.save_conversation()
continue
if query.lower().startswith('load '):
filename = query[5:].strip()
self.load_conversation(filename)
continue
6.多型号支持
动态切换模型:
def set_model(self, model_name: str):
"""Change the LLM model being used."""
valid_models = [
'gemini-2.0-flash-001',
'gemini-2.0-pro-001',
'gemini-1.5-flash',
'gemini-1.5-pro'
]
if model_name not in valid_models:
print(f"Invalid model. Choose from: {valid_models}")
return
self.current_model = model_name
print(f"Switched to model: {model_name}")
7.工具使用分析
跟踪正在使用的工具:
from collections import Counter
class MCPClient:
def __init__(self):
# ... existing code ...
self.tool_usage_stats = Counter()
async def _execute_tool_call(self, tool_name: str, tool_args: dict):
self.tool_usage_stats[tool_name] += 1
# ... rest of existing code ...
def show_stats(self):
"""Display tool usage statistics."""
print("\n=== Tool Usage Statistics ===")
for tool, count in self.tool_usage_stats.most_common():
print(f"{tool}: {count} times")
8.自定义系统提示
添加个性或专业行为:
def __init__(self, system_prompt=None):
# ... existing code ...
if system_prompt:
# For Gemini, add as first user message
self.conversation_history.append(
types.Content(
role='user',
parts=[types.Part.from_text(text=f"System: {system_prompt}")]
)
)
用途:
client = MCPClient(
system_prompt="You are a helpful coding assistant. Always explain your reasoning."
)
9.异步工具执行
并行执行多个工具:
async def _execute_tool_calls_parallel(self, tool_calls):
"""Execute multiple tool calls concurrently."""
tasks = [
self._execute_tool_call(call.name, call.args)
for call in tool_calls
]
return await asyncio.gather(*tasks)
10.Web API服务器
将您的客户端变成API:
uv add fastapi uvicorn
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
client = None # Initialize globally
class QueryRequest(BaseModel):
query: str
session_id: str = "default"
@app.post("/query")
async def process_query_endpoint(request: QueryRequest):
try:
response = await client.process_query(request.query)
return {"response": response, "session_id": request.session_id}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/clear")
async def clear_history_endpoint():
client.clear_history()
return {"status": "cleared"}
# Run with: uvicorn your_file:app --reload
额外资源
官方文件
- MCP文件: 模型上下文协议.io
- MCP Python SDK: github/modelcontextprotocol/python-sdk
- Google Gemini API: ai.google.dev/gemini-api/docs
- API人类克劳德: docs.anthopic.com
- uv包管理器: docs.astral.sh/uv
MCP服务器示例
试试这些开源MCP服务器:
-
文件系统服务器:浏览文件和目录
git clone https://github.com/modelcontextprotocol/servers cd servers/src/filesystem uv run client.py path/to/servers/src/filesystem/index.js -
GitHub服务器:与GitHub仓库交互
# Install from MCP servers repository npm install @modelcontextprotocol/server-github -
PostgreSQL服务器:查询数据库
# Python-based server pip install mcp-server-postgres -
Web搜索服务器:搜索互联网
# Check MCP server directory for web search implementations
社区与支持
- MCP GitHub讨论:提问并分享项目
- Discord 的中文翻译是“不和谐”或“纷争”。:加入MCP社区(官方文档中的链接)
- 堆栈溢出:用标记问题
model-context-protocol
学习资源
- Asyncio教程: realpython.com/async-io-python
- JSON模式指南: json-schema.org/learn
- LLM函数调用:了解AI如何使用工具
- Python类型提示: mypy.readthedocs.io
结论
现在,您已经从头开始构建了一个完整的、可用于生产的MCP客户端!本指南涵盖了:
✅ 设置:从零到运行(分钟)
✅ 代码架构:理解高级设计
✅ 逐行浏览:深入了解每个功能
✅ 多提供商支持:双子座和克劳德的替代品
✅ 测试与调试:确保一切正常
✅ 扩展:进一步发展的想法
关键要点
- MCP规范了工具访问 -一个协议,多个AI提供商
- 异步是必不可少的 -Stdio通信需要async/await
- 对话历史记录启用上下文 -AI会记住之前的转弯
- 工具格式转换至关重要 -每个LLM都有特定的要求
- 错误处理可防止崩溃 -健壮的代码处理边缘情况
接下来是什么?
- 构建自己的MCP服务器 -根据您的需求创建自定义工具
- 尝试不同的模型 -比较双子座、克劳德、GPT-4
- 添加高级功能 -流媒体、GUI、持久性
- 分享您的创作 -回馈社区
编码愉快! 🚀
许可证
本指南和代码作为教育资源提供。请随意使用、修改和分发。
贡献
发现问题或想改进本指南?
- 复刻仓库
- 进行更改
- 提交拉取请求
- 或者打开一个问题进行讨论
版本: 1.0.0
最后更新:2025年12月
作者:与Claude和MCP社区共同打造
