一文读懂如何使用MCP创建客户端

话接上文:一文读懂如何使用MCP创建服务器

本篇文章我们接着讲MCP客户端的创建过程,参考MCP官方资料

如何创建MCP客户端

开始构建自己的客户端,该客户端可以与所有 MCP 服务器集成。

在本教程中,您将学习如何构建一个由 LLM 驱动的聊天机器人客户端,该客户端可以连接到 MCP 服务器。最好先完成一次服务器快速入门 ,该教程将引导您完成构建第一个服务器的基本步骤。

系统要求

开始之前,请确保您的系统满足以下要求:

  • Mac 或 Windows 电脑
  • 已安装的最新 Python 版本
  • 已安装 uv 的最新版本

设置环境

首先,使用 uv 创建一个新的 Python 项目:

# 项目初始化
uv init mcp-client
cd mcp-client

# 创建虚拟环境
uv venv

# 激活虚拟环境
# On Windows:
.venv\Scripts\activate
# On Unix or MacOS:
source .venv/bin/activate

# 安装工具库
uv add mcp anthropic python-dotenv

# 删除无用main.py
# On Windows:
del main.py
# On Unix or MacOS:
rm main.py

# 创建客户端脚本
touch client.py

设置您的 API 密钥

您需要从 Anthropic 控制台获取 Anthropic API 密钥。

创建一个 .env 文件来存储它:

# Create .env file
touch .env

将你的密钥添加到 .env 文件中:

ANTHROPIC_API_KEY=<your key here>

将 .env 添加到你的 .gitignore 中:

echo ".env" >> .gitignore

创建客户端

导入客户端的依赖库

首先,让我们设置导入并创建基本的客户端类:

import asyncio  # 异步IO模块,用于处理异步任务
from typing import Optional  # 类型注解支持,表示可选类型
from contextlib import AsyncExitStack  # 异步上下文管理器,用于安全地管理资源释放

# 导入 MCP 协议相关模块
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client  # 基于标准输入输出的客户端通信方式

# 导入 Anthropic 提供的 Python SDK,用于访问 Claude 模型
from anthropic import Anthropic
# 从 dotenv 加载环境变量(例如 API Key)
from dotenv import load_dotenv

# 从 .env 文件中加载环境变量(如 API 密钥等敏感信息)
load_dotenv()


class MCPClient:
    def __init__(self):
        # 初始化会话和资源管理对象
        self.session: Optional[ClientSession] = None  # 存储与 MCP 服务器的会话对象
        self.exit_stack = AsyncExitStack()  # 异步资源管理栈,确保资源正确释放
        self.anthropic = Anthropic()  # 创建 Anthropic 客户端实例,用于调用 Claude 模型

服务器连接管理

接下来,我们将实现连接到 MCP 服务器的方法:

    async def connect_to_server(self, server_script_path: str):
        """连接到 MCP 服务器

        参数:
            server_script_path: 服务器脚本路径(.py 或 .js 文件)
        """
        # 判断服务器脚本是 Python 还是 JavaScript 文件
        is_python = server_script_path.endswith('.py')
        is_js = server_script_path.endswith('.js')

        # 如果不是 .py 或 .js 文件,抛出异常
        if not (is_python or is_js):
            raise ValueError("Server script must be a .py or .js file")

        # 根据文件类型选择执行命令:Python 或 Node.js
        command = "python" if is_python else "node"

        # 构建服务器启动参数
        server_params = StdioServerParameters(
            command=command,  # 执行命令
            args=[server_script_path],  # 启动参数,即服务器脚本路径
            env=None  # 环境变量,使用当前进程的环境变量
        )

        # 使用 stdio_client 启动服务器并建立通信管道
        stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
        self.stdio, self.write = stdio_transport  # 保存 stdin/stdout 和写入函数

        # 创建 ClientSession 实例,并绑定通信通道
        self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))

        # 初始化会话(发送初始化请求)
        await self.session.initialize()

        # 获取服务器提供的可用工具列表
        response = await self.session.list_tools()
        tools = response.tools  # 保存返回的工具列表

        print("\nConnected to server with tools:", [tool.name for tool in tools])

查询处理逻辑

现在让我们添加处理查询和处理工具调用的核心功能,示例客户端的核心逻辑就在这里

    async def process_query(self, query: str) -> str:
        """Process a query using Claude and available tools"""
        # 初始化消息列表,包含用户的查询内容
        messages = [
            {
                "role": "user",
                "content": query
            }
        ]

        # 获取可用的工具列表
        response = await self.session.list_tools()
        # 将响应中的工具信息提取出来,构建一个包含工具名称、描述和输入模式的字典列表
        available_tools = [{
            "name": tool.name,
            "description": tool.description,
            "input_schema": tool.inputSchema
        } for tool in response.tools]

        # 第一次调用Claude API,发送消息并获取响应
        response = self.anthropic.messages.create(
            model="claude-3-5-sonnet-20241022",  # 使用的AI模型
            max_tokens=1000,  # 最大生成token数
            messages=messages,  # 消息列表
            tools=available_tools  # 可用工具列表
        )

        # 处理响应并处理工具调用
        final_text = []  # 存储最终输出的文本

        # 遍历响应内容
        for content in response.content:
            if content.type == 'text':  # 如果是纯文本
                final_text.append(content.text)  # 添加到最终文本列表中
            elif content.type == 'tool_use':  # 如果是工具调用
                tool_name = content.name  # 工具名称
                tool_args = content.input  # 工具参数

                # 执行工具调用
                result = await self.session.call_tool(tool_name, tool_args)
                # 记录调用工具的信息
                final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")

                # 如果有文本内容,则将其添加到消息列表中
                if hasattr(content, 'text') and content.text:
                    messages.append({
                        "role": "assistant",
                        "content": content.text
                    })
                # 将工具调用结果添加到消息列表中
                messages.append({
                    "role": "user",
                    "content": result.content
                })

                # 再次调用Claude API,使用更新后的消息列表获取下一轮响应
                response = self.anthropic.messages.create(
                    model="claude-3-5-sonnet-20241022",  # 使用的AI模型
                    max_tokens=1000,  # 最大生成token数
                    messages=messages,  # 更新后的消息列表
                )

                # 将新的响应内容添加到最终文本列表中
                final_text.append(response.content[0].text)

        return "\n".join(final_text)  # 返回所有文本内容的拼接结果

交互式聊天界面

现在我们将添加聊天循环和清理功能:

async def chat_loop(self):
    """运行一个交互式的聊天循环"""
    # 提示用户客户端已启动,可以输入问题或输入 'quit' 退出
    print("\nMCP Client Started!")
    print("Type your queries or 'quit' to exit.")

    # 开始无限循环,持续接收用户输入
    while True:
        try:
            # 提示用户输入查询内容,并去除前后空格
            query = input("\nQuery: ").strip()

            # 如果用户输入 "quit"(不区分大小写),则退出循环
            if query.lower() == 'quit':
                break

            # 调用 process_query 方法处理用户输入,获取响应
            response = await self.process_query(query)
            # 打印模型返回的响应结果
            print("\n" + response)

        except Exception as e:
            # 捕获并打印任何异常信息
            print(f"\nError: {str(e)}")


async def cleanup(self):
    """清理资源"""
    # 关闭异步资源管理栈,释放所有占用的资源(如关闭子进程、网络连接等)
    await self.exit_stack.aclose()

主入口点

最后,我们将添加主要的执行逻辑:

async def main():
    # 检查命令行参数是否提供了服务器脚本路径,如果没有则打印使用说明并退出
    if len(sys.argv) < 2:
        print("Usage: python client.py <path_to_server_script>")
        sys.exit(1)

    # 创建 MCPClient 实例
    client = MCPClient()

    # 尝试连接到服务器并进入聊天循环
    try:
        await client.connect_to_server(sys.argv[1])  # 使用提供的服务器脚本路径连接服务器
        await client.chat_loop()  # 进入与服务器的持续交互循环
    finally:
        await client.cleanup()  # 确保在程序结束时执行清理操作(如关闭连接)


# 如果该文件作为主程序运行,则执行以下内容
if __name__ == "__main__":
    import sys  # 导入sys模块,用于访问解释器相关的变量和函数

    asyncio.run(main())  # 使用asyncio运行main协程函数,启动事件循环

你可以在这里找到完整的 client.py 文件 。带中文注释的如下:

import asyncio  # 异步IO模块,用于处理异步任务
from typing import Optional  # 类型注解支持,表示可选类型
from contextlib import AsyncExitStack  # 异步上下文管理器,用于安全地管理资源释放

# 导入 MCP 协议相关模块
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client  # 基于标准输入输出的客户端通信方式

# 导入 Anthropic 提供的 Python SDK,用于访问 Claude 模型
from anthropic import Anthropic
# 从 dotenv 加载环境变量(例如 API Key)
from dotenv import load_dotenv

# 从 .env 文件中加载环境变量(如 API 密钥等敏感信息)
load_dotenv()


class MCPClient:
    def __init__(self):
        # 初始化会话和资源管理对象
        self.session: Optional[ClientSession] = None  # 存储与 MCP 服务器的会话对象
        self.exit_stack = AsyncExitStack()  # 异步资源管理栈,确保资源正确释放
        self.anthropic = Anthropic()  # 创建 Anthropic 客户端实例,用于调用 Claude 模型

    async def connect_to_server(self, server_script_path: str):
        """连接到 MCP 服务器

        参数:
            server_script_path: 服务器脚本路径(.py 或 .js 文件)
        """
        # 判断服务器脚本是 Python 还是 JavaScript 文件
        is_python = server_script_path.endswith('.py')
        is_js = server_script_path.endswith('.js')

        # 如果不是 .py 或 .js 文件,抛出异常
        if not (is_python or is_js):
            raise ValueError("Server script must be a .py or .js file")

        # 根据文件类型选择执行命令:Python 或 Node.js
        command = "python" if is_python else "node"

        # 构建服务器启动参数
        server_params = StdioServerParameters(
            command=command,  # 执行命令
            args=[server_script_path],  # 启动参数,即服务器脚本路径
            env=None  # 环境变量,使用当前进程的环境变量
        )

        # 使用 stdio_client 启动服务器并建立通信管道
        stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
        self.stdio, self.write = stdio_transport  # 保存 stdin/stdout 和写入函数

        # 创建 ClientSession 实例,并绑定通信通道
        self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))

        # 初始化会话(发送初始化请求)
        await self.session.initialize()

        # 获取服务器提供的可用工具列表
        response = await self.session.list_tools()
        tools = response.tools  # 保存返回的工具列表

        print("\nConnected to server with tools:", [tool.name for tool in tools])
    async def process_query(self, query: str) -> str:
        """Process a query using Claude and available tools"""
        # 初始化消息列表,包含用户的查询内容
        messages = [
            {
                "role": "user",
                "content": query
            }
        ]

        # 获取可用的工具列表
        response = await self.session.list_tools()
        # 将响应中的工具信息提取出来,构建一个包含工具名称、描述和输入模式的字典列表
        available_tools = [{
            "name": tool.name,
            "description": tool.description,
            "input_schema": tool.inputSchema
        } for tool in response.tools]

        # 第一次调用Claude API,发送消息并获取响应
        response = self.anthropic.messages.create(
            model="claude-3-5-sonnet-20241022",  # 使用的AI模型
            max_tokens=1000,  # 最大生成token数
            messages=messages,  # 消息列表
            tools=available_tools  # 可用工具列表
        )

        # 处理响应并处理工具调用
        final_text = []  # 存储最终输出的文本

        # 遍历响应内容
        for content in response.content:
            if content.type == 'text':  # 如果是纯文本
                final_text.append(content.text)  # 添加到最终文本列表中
            elif content.type == 'tool_use':  # 如果是工具调用
                tool_name = content.name  # 工具名称
                tool_args = content.input  # 工具参数

                # 执行工具调用
                result = await self.session.call_tool(tool_name, tool_args)
                # 记录调用工具的信息
                final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")

                # 如果有文本内容,则将其添加到消息列表中
                if hasattr(content, 'text') and content.text:
                    messages.append({
                        "role": "assistant",
                        "content": content.text
                    })
                # 将工具调用结果添加到消息列表中
                messages.append({
                    "role": "user",
                    "content": result.content
                })

                # 再次调用Claude API,使用更新后的消息列表获取下一轮响应
                response = self.anthropic.messages.create(
                    model="claude-3-5-sonnet-20241022",  # 使用的AI模型
                    max_tokens=1000,  # 最大生成token数
                    messages=messages,  # 更新后的消息列表
                )

                # 将新的响应内容添加到最终文本列表中
                final_text.append(response.content[0].text)

        return "\n".join(final_text)  # 返回所有文本内容的拼接结果


async def chat_loop(self):
    """运行一个交互式的聊天循环"""
    # 提示用户客户端已启动,可以输入问题或输入 'quit' 退出
    print("\nMCP Client Started!")
    print("Type your queries or 'quit' to exit.")

    # 开始无限循环,持续接收用户输入
    while True:
        try:
            # 提示用户输入查询内容,并去除前后空格
            query = input("\nQuery: ").strip()

            # 如果用户输入 "quit"(不区分大小写),则退出循环
            if query.lower() == 'quit':
                break

            # 调用 process_query 方法处理用户输入,获取响应
            response = await self.process_query(query)
            # 打印模型返回的响应结果
            print("\n" + response)

        except Exception as e:
            # 捕获并打印任何异常信息
            print(f"\nError: {str(e)}")


async def cleanup(self):
    """清理资源"""
    # 关闭异步资源管理栈,释放所有占用的资源(如关闭子进程、网络连接等)
    await self.exit_stack.aclose()



async def main():
    # 检查命令行参数是否提供了服务器脚本路径,如果没有则打印使用说明并退出
    if len(sys.argv) < 2:
        print("Usage: python client.py <path_to_server_script>")
        sys.exit(1)

    # 创建 MCPClient 实例
    client = MCPClient()

    # 尝试连接到服务器并进入聊天循环
    try:
        await client.connect_to_server(sys.argv[1])  # 使用提供的服务器脚本路径连接服务器
        await client.chat_loop()  # 进入与服务器的持续交互循环
    finally:
        await client.cleanup()  # 确保在程序结束时执行清理操作(如关闭连接)


# 如果该文件作为主程序运行,则执行以下内容
if __name__ == "__main__":
    import sys  # 导入sys模块,用于访问解释器相关的变量和函数

    asyncio.run(main())  # 使用asyncio运行main协程函数,启动事件循环

运行客户端

要在任何 MCP 服务器上运行您的客户端:

uv run client.py path/to/server.py # python server
uv run client.py path/to/build/index.js # node server

如果您要从服务器快速入门继续气象教程,您的命令可能看起来像这样: python client.py …/quickstart-resources/weather-server-python/weather.py

客户端将:

  1. 连接到指定的服务器
  2. 列出可用的工具
  3. 开始一个互动聊天会话,您可以:
    • 输入查询
    • 查看工具执行
    • 从 Claude 获取响应

这里是从服务器快速入门连接到天气服务器后应该看起来的样子:

在这里插入图片描述

客户端的工作原理

MCP 架构参考图:
在这里插入图片描述

当您提交查询时:

  • 客户端从服务器获取可用工具列表
  • 您的查询连同工具描述一起发送给 Claude大模型
  • Claude大模型 决定使用哪些工具(如果有)
  • 客户通过服务器执行任何请求的工具调用
  • 工具响应结果发送回 Claude大模型
  • Claude大模型提供自然语言响应
  • 响应显示给您

未完待续。欢迎关注,下一期介绍:如何不自定义客户端和服务器,用户如何使用MCP?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值