MCP客户端构建工具

一个用于构建和运行MCP(模型上下文协议)客户端的工具,支持与多种LLM提供商(如Gemini、Claude)集成,实现上下文感知的对话和工具调用。

  • by Enbros-Technologies
  • Python
  • 本地部署
  • 更新于
在 GitHub 查看 Enbros-Technologies/mcp-client

使用说明

从头开始构建MCP客户端:完整指南

目录

  1. 简介&你将构建什么
  2. 先决条件
  3. 快速入门(适用于经验丰富的开发人员)
  4. 从头开始详细设置
  5. 代码架构概述
  6. 详细代码演练
  7. 使用不同的LLM提供者
  8. 运行客户端
  9. 测试您的设置
  10. 故障排除
  11. 项目结构参考
  12. 后续步骤和扩展
  13. 额外资源

简介&你将构建什么

什么是MCP(模型上下文协议)?

这 模型上下文协议(MCP) 是一个开放协议,规范了应用程序如何向大型语言模型(LLM)提供上下文。把它想象成一个通用的翻译器,让人工智能助手(如Claude、ChatGPT或Gemini)以一致的方式与外部工具、数据库和服务进行交互。

这个客户在做什么

此MCP客户端是 对话式人工智能界面 即:

  • 🔌 连接到MCP服务器 提供工具(如文件系统、数据库、API)
  • 💬 维护对话历史记录 用于上下文感知交互
  • 🛠️ 执行工具调用 当LLM需要时自动执行
  • 🔄 处理多回合对话 智能跟进
  • 🎯 与多家LLM提供商合作 (双子座、克劳德或其他)

主要特点

特性 描述
异步架构 无阻塞I/O,可与MCP服务器高效通信
工具调用循环 在一次对话中自动执行多个工具调用
对话记忆 维护完整的对话历史记录,以获取上下文响应
供应商不可知 易于在不同的LLM提供商(Gemini、Claude等)之间交换
错误处理 强大的错误恢复和信息丰富的错误消息
交互式CLI 用于测试和探索的简单命令行界面

先决条件

所需软件

  1. Python 3.11或更高版本 (3.13推荐)

    • 检查您的版本: python --version 或 python3 --version
    • 下载地址: python.org
  2. 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):创建异步上下文管理器,该管理器:

    1. 将MCP服务器作为子进程启动
    2. 连接到其stdin/stdout
    3. 返回读/写流
  • 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或空,则返回None
  • hasattr(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)::如果是纯字符串,请尝试解析为JSON
  • else::最后手段-将任何内容转换为字符串

这可以处理边缘情况 如数值结果、布尔结果等。


            # 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 API
  • model='gemini-2.0-flash-001':指定要使用的模型
    • 2.0-flash 快速且经济高效
    • 你可以使用 gemini-2.0-pro-001 用于更复杂的推理
  • contents=self.conversation_history:发送完整的对话以了解上下文
  • tools=self.function_declarations:告诉Gemini有哪些可用的工具

发生了什么: Gemini分析对话并决定是否:

  1. 直接用文本回复,或者
  2. 先调用一个或多个工具

        # 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可能:

  1. 调用工具
  2. 获取结果
  3. 根据该结果调用另一个工具
  4. 得到那个结果
  5. 最后用文本回复

这个循环处理整个链条。


            # 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

分解它:

  • 将两条消息添加到历史记录中:
    1. AI的函数调用请求(role='model')
    2. 工具的响应(role='tool')
  • 集合 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有几个原因:

  1. 免费层:为开发和测试提供充足的免费配额
  2. 最新功能:Gemini 2.0具有出色的函数调用支持
  3. 快速响应:Flash型号针对速度进行了优化
  4. 简单SDK:谷歌 google-genai 该包易于使用
  5. 良好的文件:清晰的示例和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 文件。

修复:

  1. 创建 .env 项目根目录中的文件
  2. 添加: GEMINI_API_KEY=your_actual_key
  3. 重新启动客户端

错误:“找不到模块:mcp”或“google”

原因: 未安装依赖项。

修复:

uv add mcp google-genai python-dotenv

错误:“无法连接到服务器”

可能的原因:

  1. 服务器路径错误
  2. 服务器有错误
  3. 缺少服务器依赖项

修复:

# 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陷入循环
  • 工具逻辑中的错误

修复:

  1. 检查工具结果格式
  2. 尝试 clear 重置对话
  3. 增加 max_iterations 如果需要(但要调查根本原因)

工具未被调用

症状: AI用文本而不是使用工具进行响应。

原因:

  1. 查询不够具体
  2. 工具描述不清楚
  3. 工具架构有错误

修复:

  • 明确:“使用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.py
  • pyproject.toml
  • uv.lock
  • README.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服务器示例

试试这些开源MCP服务器:

  1. 文件系统服务器:浏览文件和目录

    git clone https://github.com/modelcontextprotocol/servers
    cd servers/src/filesystem
    uv run client.py path/to/servers/src/filesystem/index.js
    
  2. GitHub服务器:与GitHub仓库交互

    # Install from MCP servers repository
    npm install @modelcontextprotocol/server-github
    
  3. PostgreSQL服务器:查询数据库

    # Python-based server
    pip install mcp-server-postgres
    
  4. Web搜索服务器:搜索互联网

    # Check MCP server directory for web search implementations
    

社区与支持

  • MCP GitHub讨论:提问并分享项目
  • Discord 的中文翻译是“不和谐”或“纷争”。:加入MCP社区(官方文档中的链接)
  • 堆栈溢出:用标记问题 model-context-protocol

学习资源


结论

现在,您已经从头开始构建了一个完整的、可用于生产的MCP客户端!本指南涵盖了:

✅ 设置:从零到运行(分钟)
✅ 代码架构:理解高级设计
✅ 逐行浏览:深入了解每个功能
✅ 多提供商支持:双子座和克劳德的替代品
✅ 测试与调试:确保一切正常
✅ 扩展:进一步发展的想法

关键要点

  1. MCP规范了工具访问 -一个协议,多个AI提供商
  2. 异步是必不可少的 -Stdio通信需要async/await
  3. 对话历史记录启用上下文 -AI会记住之前的转弯
  4. 工具格式转换至关重要 -每个LLM都有特定的要求
  5. 错误处理可防止崩溃 -健壮的代码处理边缘情况

接下来是什么?

  1. 构建自己的MCP服务器 -根据您的需求创建自定义工具
  2. 尝试不同的模型 -比较双子座、克劳德、GPT-4
  3. 添加高级功能 -流媒体、GUI、持久性
  4. 分享您的创作 -回馈社区

编码愉快! 🚀


许可证

本指南和代码作为教育资源提供。请随意使用、修改和分发。

贡献

发现问题或想改进本指南?

  1. 复刻仓库
  2. 进行更改
  3. 提交拉取请求
  4. 或者打开一个问题进行讨论

版本: 1.0.0
最后更新:2025年12月
作者:与Claude和MCP社区共同打造