├── nangeAGICode ├── search_mysql_filesystem_chat │ ├── __init__.py │ ├── .env │ ├── test │ │ └── duckduckgoTest.py │ ├── servers_config.json │ ├── mcp_server_duckduckgo.py │ ├── mcp_server_mysql.py │ ├── mcp_server_fetch.py │ └── client_chat.py ├── filesystem_chat │ ├── .env │ ├── servers_config.json │ └── client.py ├── mysql_chat │ ├── servers_config.json │ ├── .env │ ├── client_basic.py │ ├── server.py │ └── client_chat.py ├── mysql_filesystem_chat │ ├── servers_config.json │ ├── server.py │ └── client_chat.py ├── basic │ ├── client.py │ └── server.py └── filesystem_basic │ └── client.py ├── 01.png ├── 02.png ├── requirements.txt └── README.md /nangeAGICode/search_mysql_filesystem_chat/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NanGePlus/MCPTest/HEAD/01.png -------------------------------------------------------------------------------- /02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NanGePlus/MCPTest/HEAD/02.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-dotenv==1.0.1 2 | requests==2.32.3 3 | mcp==1.1.0 4 | uvicorn==0.32.1 5 | asyncio==3.4.3 6 | duckduckgo-search==6.4.2 7 | mcp-server-fetch==0.6.2 8 | -------------------------------------------------------------------------------- /nangeAGICode/filesystem_chat/.env: -------------------------------------------------------------------------------- 1 | LLM_BASE_URL=https://yunwu.ai/v1/chat/completions 2 | LLM_API_KEY=sk-efe9OmR1Vf1h7nEjiQ2U4vYDzYPquJ5FBMK9v2XmRuUdcBzH 3 | LLM_CHAT_MODEL=gpt-4o-mini 4 | -------------------------------------------------------------------------------- /nangeAGICode/mysql_chat/servers_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "mysql": { 4 | "command": "python", 5 | "args": ["server.py"], 6 | "env": null 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /nangeAGICode/mysql_chat/.env: -------------------------------------------------------------------------------- 1 | MYSQL_HOST= 239.324.82.318 2 | MYSQL_USER= root 3 | MYSQL_PASSWORD= 123456 4 | MYSQL_DATABASE= dmp 5 | 6 | LLM_BASE_URL=https://yunwu.ai/v1/chat/completions 7 | LLM_API_KEY=sk-3TfeerxAYHW3vJzxfBZFoUdH0a1cvneQ4FLNUrYQyIUChvf0 8 | LLM_CHAT_MODEL=gpt-4o-mini 9 | -------------------------------------------------------------------------------- /nangeAGICode/filesystem_chat/servers_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "filesystem": { 4 | "command": "npx", 5 | "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/janetjiang/Desktop/agi_code/MCPTest/nangeAGICode/filesystem_chat"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /nangeAGICode/mysql_filesystem_chat/servers_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "filesystem": { 4 | "command": "npx", 5 | "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/janetjiang/Desktop/agi_code/MCPTest/nangeAGICode/mysql_filesystem_chat"] 6 | }, 7 | "mysql": { 8 | "command": "python", 9 | "args": ["server.py"], 10 | "env": null 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /nangeAGICode/search_mysql_filesystem_chat/.env: -------------------------------------------------------------------------------- 1 | MYSQL_HOST= 139.224.72.218 2 | MYSQL_USER= root 3 | MYSQL_PASSWORD= Aa112233@ 4 | MYSQL_DATABASE= dmp 5 | 6 | LLM_BASE_URL=https://yunwu.ai/v1/chat/completions 7 | LLM_API_KEY=sk-2GvbiApgIIWHlHrPOXbsA76ia5ohul2iaSoXXaJIAkEAZgHH 8 | LLM_CHAT_MODEL=gpt-4o-mini 9 | 10 | LLM_BASE_URL=http://139.224.72.218:3000/v1/chat/completions 11 | LLM_API_KEY=sk-pUWxMJdf3n2Fcxla502cAe5500Dc4a05A84e6b3dE69c0962 12 | LLM_CHAT_MODEL=qwen-max 13 | -------------------------------------------------------------------------------- /nangeAGICode/search_mysql_filesystem_chat/test/duckduckgoTest.py: -------------------------------------------------------------------------------- 1 | # pip install duckduckgo_search==6.4.2 2 | 3 | 4 | from duckduckgo_search import DDGS 5 | 6 | search_query = 'python programming' 7 | 8 | results = DDGS().text( 9 | keywords = search_query, 10 | region = "cn-zh", 11 | safesearch = 'off', 12 | timelimit = '7d', 13 | max_results=5 14 | ) 15 | 16 | # 拼接字符串 17 | results = "\n".join(f"{item['title']} - {item['href']} - {item['body']}" for item in results) 18 | 19 | print(results) 20 | 21 | 22 | -------------------------------------------------------------------------------- /nangeAGICode/search_mysql_filesystem_chat/servers_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "filesystem": { 4 | "command": "npx", 5 | "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/janetjiang/Desktop/agi_code/MCPPreview/nangeAGICode/search_mysql_filesystem_chat"] 6 | }, 7 | "mysql_mcp_server": { 8 | "command": "python", 9 | "args": ["mcp_server_mysql.py"], 10 | "env": null 11 | }, 12 | "duckduckgo_mcp_server": { 13 | "command": "python", 14 | "args": ["mcp_server_duckduckgo.py"], 15 | "env": null 16 | }, 17 | "fetch": { 18 | "command": "python", 19 | "args": ["mcp_server_fetch.py"], 20 | "env": null 21 | } 22 | } 23 | } 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /nangeAGICode/basic/client.py: -------------------------------------------------------------------------------- 1 | # 此脚本模拟了一个 mcp 客户端,通过标准输入/输出与服务器交互 2 | # 执行的主要任务: 3 | # 启动服务器 4 | # 初始化客户端与服务器的连接 5 | # 列出服务器支持的 prompts 6 | # 获取一个具体的 prompt 7 | 8 | 9 | # ClientSession 表示客户端会话,用于与服务器交互 10 | # StdioServerParameters 定义与服务器的 stdio 连接参数 11 | from mcp import ClientSession, StdioServerParameters 12 | # 提供与服务器的 stdio 连接上下文管理器 13 | from mcp.client.stdio import stdio_client 14 | import asyncio 15 | 16 | 17 | 18 | 19 | # 为 stdio 连接创建服务器参数 20 | server_params = StdioServerParameters( 21 | # 服务器执行的命令,这里是 python 22 | command="python", 23 | # 启动命令的附加参数,这里是运行 example_server.py 24 | args=["server.py"], 25 | # 环境变量,默认为 None,表示使用当前环境变量 26 | # env=None 27 | ) 28 | 29 | 30 | async def run(): 31 | # 创建与服务器的标准输入/输出连接,并返回 read 和 write 流 32 | async with stdio_client(server_params) as (read, write): 33 | # 创建一个客户端会话对象,通过 read 和 write 流与服务器交互 34 | async with ClientSession(read, write) as session: 35 | # 向服务器发送初始化请求,确保连接准备就绪 36 | # 建立初始状态,并让服务器返回其功能和版本信息 37 | capabilities = await session.initialize() 38 | print("capabilities:", capabilities) 39 | print("Supported capabilities:", capabilities.capabilities) 40 | 41 | # 请求服务器列出所有支持的 prompt 42 | # 返回包含 prompt 元信息的列表,例如名称、描述及参数 43 | prompts = await session.list_prompts() 44 | print("prompts:",prompts) 45 | 46 | # 请求服务器获取一个特定的 prompt 实例 47 | # 返回prompt 对象,包含消息和相关信息 48 | prompt = await session.get_prompt("example-prompt", arguments={"arg1": "value"}) 49 | print("prompt:", prompt) 50 | 51 | 52 | if __name__ == "__main__": 53 | # 使用 asyncio 启动异步的 run() 函数 54 | asyncio.run(run()) -------------------------------------------------------------------------------- /nangeAGICode/mysql_chat/client_basic.py: -------------------------------------------------------------------------------- 1 | # Node.js 服务器为文件系统操作实现模型上下文协议 (MCP) 2 | 3 | # ClientSession 表示客户端会话,用于与服务器交互 4 | # StdioServerParameters 定义与服务器的 stdio 连接参数 5 | from mcp import ClientSession, StdioServerParameters 6 | # 提供与服务器的 stdio 连接上下文管理器 7 | from mcp.client.stdio import stdio_client 8 | import asyncio 9 | 10 | # 定义与 mcp 协议相关的类型 11 | import mcp.types as types 12 | 13 | 14 | 15 | 16 | # 为 stdio 连接创建服务器参数 17 | server_params = StdioServerParameters( 18 | # 服务器执行的命令,这里是 python 19 | command="python", 20 | # 启动命令的附加参数,这里是运行 example_server.py 21 | args=["server.py"], 22 | # 环境变量,默认为 None,表示使用当前环境变量 23 | env=None 24 | ) 25 | 26 | 27 | async def run(): 28 | # 创建与服务器的标准输入/输出连接,并返回 read 和 write 流 29 | async with stdio_client(server_params) as (read, write): 30 | # 创建一个客户端会话对象,通过 read 和 write 流与服务器交互 31 | async with ClientSession(read, write) as session: 32 | # 向服务器发送初始化请求,确保连接准备就绪 33 | # 建立初始状态,并让服务器返回其功能和版本信息 34 | capabilities = await session.initialize() 35 | print("capabilities:", capabilities) 36 | 37 | # 请求服务器列出所有支持的资源 38 | # resources = await session.list_resources() 39 | # print("resources:",resources) 40 | 41 | # 获取某具体资源 即某张表中的内容 42 | # resource = await session.read_resource('mysql://nange_agi/data') 43 | # print("resource:",resource) 44 | 45 | # 获取可用的工具列表 46 | # tools = await session.list_tools() 47 | # print("tools:",tools) 48 | 49 | # 工具功能测试 50 | # result = await session.call_tool("execute_sql",{"query":"SHOW TABLES"}) 51 | # result = await session.call_tool("execute_sql",{"query":"SELECT * FROM nange_agi"}) 52 | # print("result:",result) 53 | 54 | 55 | if __name__ == "__main__": 56 | # 使用 asyncio 启动异步的 run() 函数 57 | asyncio.run(run()) -------------------------------------------------------------------------------- /nangeAGICode/filesystem_basic/client.py: -------------------------------------------------------------------------------- 1 | # Node.js 服务器为文件系统操作实现模型上下文协议 (MCP) 2 | 3 | # ClientSession 表示客户端会话,用于与服务器交互 4 | # StdioServerParameters 定义与服务器的 stdio 连接参数 5 | from mcp import ClientSession, StdioServerParameters 6 | # 提供与服务器的 stdio 连接上下文管理器 7 | from mcp.client.stdio import stdio_client 8 | import asyncio 9 | 10 | # 定义与 mcp 协议相关的类型 11 | import mcp.types as types 12 | 13 | 14 | 15 | 16 | # 为 stdio 连接创建服务器参数 17 | server_params = StdioServerParameters( 18 | # 服务器执行的命令,这里是 python 19 | command="npx", 20 | # 启动命令的附加参数,这里是运行 example_server.py 21 | args=["-y", "@modelcontextprotocol/server-filesystem", "/Users/janetjiang/Desktop/agi_code/MCPTest/nangeAGICode/filesystem_basic"], 22 | # 环境变量,默认为 None,表示使用当前环境变量 23 | env=None 24 | ) 25 | 26 | 27 | async def run(): 28 | # 创建与服务器的标准输入/输出连接,并返回 read 和 write 流 29 | async with stdio_client(server_params) as (read, write): 30 | # 创建一个客户端会话对象,通过 read 和 write 流与服务器交互 31 | async with ClientSession(read, write) as session: 32 | # 向服务器发送初始化请求,确保连接准备就绪 33 | # 建立初始状态,并让服务器返回其功能和版本信息 34 | capabilities = await session.initialize() 35 | # print("capabilities:", capabilities) 36 | # print("Supported capabilities:", capabilities.capabilities) 37 | 38 | # 请求服务器列出所有支持的 tools 39 | tools = await session.list_tools() 40 | print("tools:",tools) 41 | 42 | # 文件相关功能测试 43 | result = await session.call_tool("list_allowed_directories") 44 | # result = await session.call_tool("create_directory", arguments={"path": "test"}) 45 | # result = await session.call_tool("write_file", arguments={"path": "test/test1.txt","content": "这里是南哥AGI研习社。测试1。" }) 46 | # result = await session.call_tool("write_file", arguments={"path": "test/test1.txt","content": "这里是南哥AGI研习社。测试1plus。" }) 47 | # result = await session.call_tool("write_file", arguments={"path": "test/test2.txt","content": "这里是南哥AGI研习社。测试2。" }) 48 | # result = await session.call_tool("list_directory", arguments={"path": "test"}) 49 | # result = await session.call_tool("read_file", arguments={"path": "test/test1.txt"}) 50 | # result = await session.call_tool("read_multiple_files", arguments={"paths": ["test/test1.txt","test/test2.txt"]}) 51 | # result = await session.call_tool("search_files", arguments={"path": "test","pattern": "test1" }) 52 | # result = await session.call_tool("get_file_info", arguments={"path": "test/test1.txt"}) 53 | print("result:",result) 54 | 55 | 56 | if __name__ == "__main__": 57 | # 使用 asyncio 启动异步的 run() 函数 58 | asyncio.run(run()) 59 | -------------------------------------------------------------------------------- /nangeAGICode/basic/server.py: -------------------------------------------------------------------------------- 1 | # 此脚本实现了一个支持 MCP 协议的服务器: 2 | # 提供了一个名为 example-prompt 的 prompt 3 | # 支持客户端通过标准输入/输出与服务器通信 4 | # 能够响应客户端的 list_prompts 和 get_prompt 请求,返回相应的内容 5 | 6 | 7 | # Server 提供服务器实例化功能 8 | # NotificationOptions 用于配置通知选项 9 | from mcp.server import Server, NotificationOptions 10 | # 服务器初始化时的选项 11 | from mcp.server.models import InitializationOptions 12 | # 提供标准输入/输出支持,用于与外部工具交互 13 | import mcp.server.stdio 14 | # 定义与 mcp 协议相关的类型 15 | import mcp.types as types 16 | import asyncio 17 | 18 | 19 | 20 | 21 | # 创建一个名为 example-server 的服务器实例 22 | server = Server("example-server") 23 | 24 | # 注册一个回调函数,返回服务器支持的 prompt 列表 25 | @server.list_prompts() 26 | async def handle_list_prompts() -> list[types.Prompt]: 27 | # 返回一个包含 Prompt 对象的列表 28 | return [ 29 | # 定义了一个prompt,包含以下信息 30 | types.Prompt( 31 | name="example-prompt", 32 | description="An example prompt template", 33 | arguments=[ 34 | types.PromptArgument( 35 | name="arg1", 36 | description="Example argument", 37 | required=True 38 | ) 39 | ] 40 | ) 41 | ] 42 | 43 | 44 | # 注册一个回调函数,用于根据 prompt 名称和参数生成具体的 prompt 内容 45 | @server.get_prompt() 46 | async def handle_get_prompt( 47 | name: str, 48 | arguments: dict[str, str] | None 49 | ) -> types.GetPromptResult: 50 | if name != "example-prompt": 51 | raise ValueError(f"Unknown prompt: {name}") 52 | 53 | # 返回一个GetPromptResult 对象,包含 prompt 的详细信息 54 | return types.GetPromptResult( 55 | description="Example prompt", 56 | messages=[ 57 | types.PromptMessage( 58 | role="user", 59 | content=types.TextContent( 60 | type="text", 61 | text="Example prompt text" 62 | ) 63 | ) 64 | ] 65 | ) 66 | 67 | 68 | async def run(): 69 | # 使用 stdio_server 启动标准输入/输出的服务器模式 70 | # 提供 read_stream 和 write_stream 用于数据传输 71 | async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): 72 | # 启动服务器,使用初始化选项 InitializationOptions 73 | await server.run( 74 | read_stream, 75 | write_stream, 76 | InitializationOptions( 77 | server_name="example", 78 | server_version="0.1.0", 79 | # 配置服务器功能,包括通知选项和实验性功能 80 | capabilities=server.get_capabilities( 81 | notification_options=NotificationOptions(), 82 | experimental_capabilities={}, 83 | ) 84 | ) 85 | ) 86 | 87 | 88 | 89 | if __name__ == "__main__": 90 | # 使用 asyncio 运行异步的 run() 函数,启动服务器 91 | asyncio.run(run()) -------------------------------------------------------------------------------- /nangeAGICode/search_mysql_filesystem_chat/mcp_server_duckduckgo.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import os 4 | from mysql.connector import connect, Error 5 | from mcp.server import Server 6 | from mcp.types import Tool, TextContent 7 | from mcp.server.stdio import stdio_server 8 | from pydantic import AnyUrl 9 | from dotenv import load_dotenv 10 | 11 | from duckduckgo_search import DDGS 12 | 13 | 14 | 15 | 16 | 17 | # 日志相关配置 18 | logging.basicConfig( 19 | level=logging.INFO, 20 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 21 | ) 22 | logger = logging.getLogger("mysql_mcp_server") 23 | 24 | 25 | 26 | # 实例化Server 27 | app = Server("duckduckgo_mcp_server") 28 | 29 | 30 | # 声明 list_tools 函数为一个列出工具的接口 31 | # 列出可用的 MySQL 工具 32 | @app.list_tools() 33 | async def list_tools() -> list[Tool]: 34 | logger.info("Listing tools...") 35 | # 函数返回一个列表,其中包含一个 Tool 对象 36 | # 每个 Tool 对象代表一个工具,其属性定义了工具的功能和输入要求 37 | return [ 38 | Tool( 39 | # 工具的名称 40 | name="duckduckgo_web_search", 41 | # 工具的描述 42 | description="执行搜索查询", 43 | # 定义了工具的输入模式(Schema),用于描述输入数据的格式和要求 44 | inputSchema={ 45 | # 定义输入为一个 JSON 对象 46 | "type": "object", 47 | # 定义输入对象的属性 48 | "properties": { 49 | "query": { 50 | "type": "string", 51 | "description": "Search query (max 400 chars, 50 words)" 52 | }, 53 | "max_results": { 54 | "type": "number", 55 | "description": "Number of results,default 10", 56 | "default": 10 57 | }, 58 | }, 59 | # 列出输入对象的必需属性 60 | "required": ["query"] 61 | } 62 | ) 63 | ] 64 | 65 | 66 | # 声明 call_tool 函数为一个工具调用的接口 67 | # 根据传入的工具名称和参数执行相应的命令 68 | # name: 工具的名称(字符串),指定要调用的工具 69 | # arguments: 一个字典,包含工具所需的参数 70 | @app.call_tool() 71 | async def call_tool(name: str, arguments: dict) -> list[TextContent]: 72 | 73 | logger.info(f"Calling tool: {name} with arguments: {arguments}") 74 | 75 | # 检查工具名称 name 是否是 "duckduckgo_web_search" 76 | # 如果 query 为空或未提供,抛出 ValueError 异常,提示用户必须提供查询语句 77 | if name != "duckduckgo_web_search": 78 | raise ValueError(f"Unknown tool: {name}") 79 | 80 | query = arguments.get("query") 81 | max_results = arguments.get("max_results") 82 | if not query: 83 | raise ValueError("Query is required") 84 | 85 | try: 86 | results = DDGS().text( 87 | keywords=query, 88 | region="cn-zh", 89 | safesearch='off', 90 | timelimit='7d', 91 | max_results=max_results 92 | ) 93 | # 拼接字符串 94 | results = "\n".join(f"{item['title']} - {item['href']} - {item['body']}" for item in results) 95 | # 返回一个包含查询结果的 TextContent 对象 96 | return [TextContent(type="text", text=results)] 97 | 98 | # 捕获操作期间发生的任何异常 99 | except Error as e: 100 | logger.error(f"Error executing search '{query}': {e}") 101 | return [TextContent(type="text", text=f"Error executing query: {str(e)}")] 102 | 103 | 104 | # 启动 MCP服务器 105 | async def main(): 106 | logger.info("Starting DuckDuckGo Search MCP server...") 107 | 108 | # 启动 stdio_server,通过标准输入/输出(stdin/stdout)与客户端通信 109 | # async with 是异步上下文管理器,确保 stdio_server 资源在使用完成后自动清理 110 | # 返回的 (read_stream, write_stream) 是两个流对象 111 | # read_stream: 用于从客户端读取输入的流对象 112 | # write_stream: 用于向客户端发送输出的流对象 113 | async with stdio_server() as (read_stream, write_stream): 114 | try: 115 | # 异步运行 MCP 应用程序 116 | await app.run( 117 | read_stream, 118 | write_stream, 119 | # 用于初始化应用程序的选项,通常包含配置或上下文信息 120 | app.create_initialization_options() 121 | ) 122 | # 捕获运行 app.run() 时发生的所有异常 123 | except Exception as e: 124 | logger.error(f"Server error: {str(e)}", exc_info=True) 125 | raise 126 | 127 | 128 | 129 | 130 | if __name__ == "__main__": 131 | asyncio.run(main()) -------------------------------------------------------------------------------- /nangeAGICode/mysql_chat/server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import os 4 | from mysql.connector import connect, Error 5 | from mcp.server import Server 6 | from mcp.types import Resource, Tool, TextContent 7 | from mcp.server.stdio import stdio_server 8 | from pydantic import AnyUrl 9 | from dotenv import load_dotenv 10 | 11 | 12 | 13 | 14 | 15 | # 日志相关配置 16 | logging.basicConfig( 17 | level=logging.INFO, 18 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 19 | ) 20 | logger = logging.getLogger("mysql_mcp_server") 21 | 22 | 23 | # 获取数据库配置 24 | def get_db_config(): 25 | # 加载 .env 文件中的环境变量到系统环境变量中 26 | load_dotenv() 27 | # 从环境变量中获取数据库配置 28 | config = { 29 | "host": os.getenv("MYSQL_HOST"), 30 | "user": os.getenv("MYSQL_USER"), 31 | "password": os.getenv("MYSQL_PASSWORD"), 32 | "database": os.getenv("MYSQL_DATABASE") 33 | } 34 | 35 | # 检查是否存在配置中的关键字段 36 | # 记录错误信息,提示用户检查环境变量 37 | if not all([config["user"], config["password"], config["database"]]): 38 | logger.error("Missing required database configuration. Please check environment variables:") 39 | logger.error("MYSQL_USER, MYSQL_PASSWORD, and MYSQL_DATABASE are required") 40 | # 抛出一个 ValueError 异常,终止函数的执行 41 | raise ValueError("Missing required database configuration") 42 | 43 | # 配置完整,则返回包含数据库配置的字典config 44 | return config 45 | 46 | 47 | # 实例化Server 48 | app = Server("mysql_mcp_server") 49 | 50 | 51 | # 声明 list_resources 函数为一个资源列表接口 52 | # 列出 MySQL 数据库中的表并将其作为资源返回 53 | @app.list_resources() 54 | async def list_resources() -> list[Resource]: 55 | # 获取数据库配置 56 | config = get_db_config() 57 | try: 58 | # 连接数据库 59 | # with 语句用于确保连接在使用完成后自动关闭(即使发生异常) 60 | with connect(**config) as conn: 61 | # 创建一个数据库游标 cursor,用于执行 SQL 查询 62 | # with 确保游标在操作完成后自动关闭 63 | with conn.cursor() as cursor: 64 | # 执行 SQL 查询 SHOW TABLES,它列出当前数据库中的所有表 65 | cursor.execute("SHOW TABLES") 66 | # 使用 fetchall 方法获取查询结果,返回一个包含所有表名的列表 67 | tables = cursor.fetchall() 68 | logger.info(f"Found tables: {tables}") 69 | 70 | # 初始化一个空列表 resources,用于存储 Resource 对象 71 | resources = [] 72 | # 遍历 tables 列表,每次迭代处理一个表名 73 | for table in tables: 74 | # 创建一个 Resource 对象 75 | # 填充其属性: 76 | # uri: 表的唯一资源标识符 77 | # name: 资源的名称 78 | # mimeType: MIME 类型,表示资源的数据类型 79 | # description: 描述信息 80 | resources.append( 81 | Resource( 82 | uri=f"mysql://{table[0]}/data", 83 | name=f"Table: {table[0]}", 84 | mimeType="text/plain", 85 | description=f"Data in table: {table[0]}" 86 | ) 87 | ) 88 | # 在成功获取表信息并构造 Resource 对象列表后,返回 resources 89 | return resources 90 | # 发生异常,返回一个空列表,表明未能成功获取任何资源 91 | except Error as e: 92 | logger.error(f"Failed to list resources: {str(e)}") 93 | return [] 94 | 95 | 96 | # 声明 read_resource 函数为一个读取资源的接口 97 | # 根据传入的 URI 读取表的内容 98 | # uri: 表示资源的 URI,类型为 AnyUrl,确保输入是一个合法的 URL 99 | @app.read_resource() 100 | async def read_resource(uri: AnyUrl) -> str: 101 | # 获取数据库配置 102 | config = get_db_config() 103 | # 将 uri 对象转换为字符串形式,存储在 uri_str 变量中 104 | uri_str = str(uri) 105 | logger.info(f"Reading resource: {uri_str}") 106 | 107 | # 检查 uri_str 是否以 "mysql://" 开头,确保 URI 符合预期的 MySQL 资源格式 108 | if not uri_str.startswith("mysql://"): 109 | # 如果不符合,则抛出 ValueError 异常,提示 URI 格式无效 110 | raise ValueError(f"Invalid URI scheme: {uri_str}") 111 | 112 | # 将 URI 中 "mysql://" 部分去掉,并按照 '/' 分割为多个部分 113 | parts = uri_str[8:].split('/') 114 | # parts[0] 是表名,存储到变量 table 中 115 | table = parts[0] 116 | 117 | try: 118 | # 使用数据库连接库的 connect 方法,使用 config 中的配置参数连接到数据库 119 | # with 确保在连接关闭时资源被正确释放 120 | with connect(**config) as conn: 121 | # 创建一个数据库游标 cursor,用于执行 SQL 查询 122 | # with 确保游标在操作完成后自动关闭 123 | with conn.cursor() as cursor: 124 | # 执行 SQL 查询,读取表中的前 100 条记录 125 | # 注意:此处直接使用 table 变量拼接查询字符串,有潜在的 SQL 注入风险 126 | cursor.execute(f"SELECT * FROM {table} LIMIT 100") 127 | # 获取表的列名: 128 | # cursor.description 返回查询结果的列描述信息 129 | # 列表推导式提取每列的名称(desc[0]) 130 | columns = [desc[0] for desc in cursor.description] 131 | # 使用 fetchall 方法获取查询结果的所有行 132 | # 返回值 rows 是一个包含多行数据的列表,每行是一个元组 133 | rows = cursor.fetchall() 134 | # 使用列表推导式将每行数据转换为逗号分隔的字符串 135 | # map(str, row) 将行中的每个元素转换为字符串 136 | # ",".join(...) 将字符串连接成一行数据 137 | # 结果存储在 result 列表中 138 | result = [",".join(map(str, row)) for row in rows] 139 | # 将列名(作为第一行)和数据行组合成最终字符串返回 140 | return "\n".join([",".join(columns)] + result) 141 | # 捕获数据库连接或查询期间发生的任何异常 142 | except Error as e: 143 | logger.error(f"Database error reading resource {uri}: {str(e)}") 144 | raise RuntimeError(f"Database error: {str(e)}") 145 | 146 | 147 | # 声明 list_tools 函数为一个列出工具的接口 148 | # 列出可用的 MySQL 工具 149 | @app.list_tools() 150 | async def list_tools() -> list[Tool]: 151 | logger.info("Listing tools...") 152 | # 函数返回一个列表,其中包含一个 Tool 对象 153 | # 每个 Tool 对象代表一个工具,其属性定义了工具的功能和输入要求 154 | return [ 155 | Tool( 156 | # 工具的名称 157 | name="execute_sql", 158 | # 工具的描述 159 | description="Execute an SQL query on the MySQL server", 160 | # 定义了工具的输入模式(Schema),用于描述输入数据的格式和要求 161 | inputSchema={ 162 | # 定义输入为一个 JSON 对象 163 | "type": "object", 164 | # 定义输入对象的属性 165 | "properties": { 166 | # 指明此属性存储要执行的 SQL 查询 167 | "query": { 168 | "type": "string", 169 | "description": "The SQL query to execute" 170 | } 171 | }, 172 | # 列出输入对象的必需属性 173 | "required": ["query"] 174 | } 175 | ) 176 | ] 177 | 178 | 179 | # 声明 call_tool 函数为一个工具调用的接口 180 | # 根据传入的工具名称和参数执行相应的 SQL 命令 181 | # name: 工具的名称(字符串),指定要调用的工具 182 | # arguments: 一个字典,包含工具所需的参数 183 | @app.call_tool() 184 | async def call_tool(name: str, arguments: dict) -> list[TextContent]: 185 | # 获取数据库配置 186 | config = get_db_config() 187 | logger.info(f"Calling tool: {name} with arguments: {arguments}") 188 | 189 | # 检查工具名称 name 是否是 "execute_sql" 190 | # 如果 query 为空或未提供,抛出 ValueError 异常,提示用户必须提供查询语句 191 | if name != "execute_sql": 192 | raise ValueError(f"Unknown tool: {name}") 193 | 194 | query = arguments.get("query") 195 | if not query: 196 | raise ValueError("Query is required") 197 | 198 | # 使用数据库连接库的 connect 方法,使用 config 中的配置参数连接到数据库 199 | # with 确保在连接关闭时资源被正确释放 200 | try: 201 | with connect(**config) as conn: 202 | # 创建一个数据库游标 cursor,用于执行 SQL 查询 203 | # with 确保游标在操作完成后自动关闭 204 | with conn.cursor() as cursor: 205 | # 执行 SQL 查询,参数为用户提供的 query 206 | cursor.execute(query) 207 | 208 | # 检查查询是否以 "SHOW TABLES" 开头(忽略大小写和前后空格) 209 | # 这是对特定 SQL 命令的特殊处理 210 | if query.strip().upper().startswith("SHOW TABLES"): 211 | # 使用 fetchall() 获取所有结果,返回的每个结果是一个元组(包含表名) 212 | tables = cursor.fetchall() 213 | # 第一行为表头 214 | result = ["Tables_in_" + config["database"]] 215 | # 其余行为表名 216 | result.extend([table[0] for table in tables]) 217 | # 返回一个包含表名的 TextContent 对象列表,文本以换行符分隔 218 | return [TextContent(type="text", text="\n".join(result))] 219 | 220 | # 检查查询是否以 "SELECT" 开头,表明这是一个读取数据的查询 221 | elif query.strip().upper().startswith("SELECT"): 222 | # 获取列名 cursor.description 返回列的描述信息 列表推导式提取列名 223 | columns = [desc[0] for desc in cursor.description] 224 | # 获取行数据 使用 fetchall() 获取所有结果 列表推导式将每行数据转换为逗号分隔的字符串 225 | rows = cursor.fetchall() 226 | # 构建结果字符串 第一行是列名,后续是数据行 用换行符拼接各行,形成完整的 CSV 格式 227 | result = [",".join(map(str, row)) for row in rows] 228 | # 返回一个包含查询结果的 TextContent 对象 229 | return [TextContent(type="text", text="\n".join([",".join(columns)] + result))] 230 | 231 | # 对于非 "SHOW TABLES" 和非 "SELECT" 的查询(如 INSERT, UPDATE, DELETE 等) 232 | else: 233 | # 提交事务以确保更改生效 234 | conn.commit() 235 | # 返回成功消息,包含受影响的行数 cursor.rowcount 236 | return [TextContent(type="text", text=f"Query executed successfully. Rows affected: {cursor.rowcount}")] 237 | # 捕获数据库操作期间发生的任何异常 238 | except Error as e: 239 | logger.error(f"Error executing SQL '{query}': {e}") 240 | return [TextContent(type="text", text=f"Error executing query: {str(e)}")] 241 | 242 | 243 | # 启动 MCP服务器 244 | async def main(): 245 | logger.info("Starting MySQL MCP server...") 246 | # 获取数据库连接的配置信息 247 | config = get_db_config() 248 | logger.info(f"Database config: {config['host']}/{config['database']} as {config['user']}") 249 | 250 | # 启动 stdio_server,通过标准输入/输出(stdin/stdout)与客户端通信 251 | # async with 是异步上下文管理器,确保 stdio_server 资源在使用完成后自动清理 252 | # 返回的 (read_stream, write_stream) 是两个流对象 253 | # read_stream: 用于从客户端读取输入的流对象 254 | # write_stream: 用于向客户端发送输出的流对象 255 | async with stdio_server() as (read_stream, write_stream): 256 | try: 257 | # 异步运行 MCP 应用程序 258 | await app.run( 259 | read_stream, 260 | write_stream, 261 | # 用于初始化应用程序的选项,通常包含配置或上下文信息 262 | app.create_initialization_options() 263 | ) 264 | # 捕获运行 app.run() 时发生的所有异常 265 | except Exception as e: 266 | logger.error(f"Server error: {str(e)}", exc_info=True) 267 | raise 268 | 269 | 270 | 271 | 272 | if __name__ == "__main__": 273 | asyncio.run(main()) -------------------------------------------------------------------------------- /nangeAGICode/mysql_filesystem_chat/server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import os 4 | from mysql.connector import connect, Error 5 | from mcp.server import Server 6 | from mcp.types import Resource, Tool, TextContent 7 | from mcp.server.stdio import stdio_server 8 | from pydantic import AnyUrl 9 | from dotenv import load_dotenv 10 | 11 | 12 | 13 | 14 | 15 | # 日志相关配置 16 | logging.basicConfig( 17 | level=logging.INFO, 18 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 19 | ) 20 | logger = logging.getLogger("mysql_mcp_server") 21 | 22 | 23 | # 获取数据库配置 24 | def get_db_config(): 25 | # 加载 .env 文件中的环境变量到系统环境变量中 26 | load_dotenv() 27 | # 从环境变量中获取数据库配置 28 | config = { 29 | "host": os.getenv("MYSQL_HOST"), 30 | "user": os.getenv("MYSQL_USER"), 31 | "password": os.getenv("MYSQL_PASSWORD"), 32 | "database": os.getenv("MYSQL_DATABASE") 33 | } 34 | 35 | # 检查是否存在配置中的关键字段 36 | # 记录错误信息,提示用户检查环境变量 37 | if not all([config["user"], config["password"], config["database"]]): 38 | logger.error("Missing required database configuration. Please check environment variables:") 39 | logger.error("MYSQL_USER, MYSQL_PASSWORD, and MYSQL_DATABASE are required") 40 | # 抛出一个 ValueError 异常,终止函数的执行 41 | raise ValueError("Missing required database configuration") 42 | 43 | # 配置完整,则返回包含数据库配置的字典config 44 | return config 45 | 46 | 47 | # 实例化Server 48 | app = Server("mysql_mcp_server") 49 | 50 | 51 | # 声明 list_resources 函数为一个资源列表接口 52 | # 列出 MySQL 数据库中的表并将其作为资源返回 53 | @app.list_resources() 54 | async def list_resources() -> list[Resource]: 55 | # 获取数据库配置 56 | config = get_db_config() 57 | try: 58 | # 连接数据库 59 | # with 语句用于确保连接在使用完成后自动关闭(即使发生异常) 60 | with connect(**config) as conn: 61 | # 创建一个数据库游标 cursor,用于执行 SQL 查询 62 | # with 确保游标在操作完成后自动关闭 63 | with conn.cursor() as cursor: 64 | # 执行 SQL 查询 SHOW TABLES,它列出当前数据库中的所有表 65 | cursor.execute("SHOW TABLES") 66 | # 使用 fetchall 方法获取查询结果,返回一个包含所有表名的列表 67 | tables = cursor.fetchall() 68 | logger.info(f"Found tables: {tables}") 69 | 70 | # 初始化一个空列表 resources,用于存储 Resource 对象 71 | resources = [] 72 | # 遍历 tables 列表,每次迭代处理一个表名 73 | for table in tables: 74 | # 创建一个 Resource 对象 75 | # 填充其属性: 76 | # uri: 表的唯一资源标识符 77 | # name: 资源的名称 78 | # mimeType: MIME 类型,表示资源的数据类型 79 | # description: 描述信息 80 | resources.append( 81 | Resource( 82 | uri=f"mysql://{table[0]}/data", 83 | name=f"Table: {table[0]}", 84 | mimeType="text/plain", 85 | description=f"Data in table: {table[0]}" 86 | ) 87 | ) 88 | # 在成功获取表信息并构造 Resource 对象列表后,返回 resources 89 | return resources 90 | # 发生异常,返回一个空列表,表明未能成功获取任何资源 91 | except Error as e: 92 | logger.error(f"Failed to list resources: {str(e)}") 93 | return [] 94 | 95 | 96 | # 声明 read_resource 函数为一个读取资源的接口 97 | # 根据传入的 URI 读取表的内容 98 | # uri: 表示资源的 URI,类型为 AnyUrl,确保输入是一个合法的 URL 99 | @app.read_resource() 100 | async def read_resource(uri: AnyUrl) -> str: 101 | # 获取数据库配置 102 | config = get_db_config() 103 | # 将 uri 对象转换为字符串形式,存储在 uri_str 变量中 104 | uri_str = str(uri) 105 | logger.info(f"Reading resource: {uri_str}") 106 | 107 | # 检查 uri_str 是否以 "mysql://" 开头,确保 URI 符合预期的 MySQL 资源格式 108 | if not uri_str.startswith("mysql://"): 109 | # 如果不符合,则抛出 ValueError 异常,提示 URI 格式无效 110 | raise ValueError(f"Invalid URI scheme: {uri_str}") 111 | 112 | # 将 URI 中 "mysql://" 部分去掉,并按照 '/' 分割为多个部分 113 | parts = uri_str[8:].split('/') 114 | # parts[0] 是表名,存储到变量 table 中 115 | table = parts[0] 116 | 117 | try: 118 | # 使用数据库连接库的 connect 方法,使用 config 中的配置参数连接到数据库 119 | # with 确保在连接关闭时资源被正确释放 120 | with connect(**config) as conn: 121 | # 创建一个数据库游标 cursor,用于执行 SQL 查询 122 | # with 确保游标在操作完成后自动关闭 123 | with conn.cursor() as cursor: 124 | # 执行 SQL 查询,读取表中的前 100 条记录 125 | # 注意:此处直接使用 table 变量拼接查询字符串,有潜在的 SQL 注入风险 126 | cursor.execute(f"SELECT * FROM {table} LIMIT 100") 127 | # 获取表的列名: 128 | # cursor.description 返回查询结果的列描述信息 129 | # 列表推导式提取每列的名称(desc[0]) 130 | columns = [desc[0] for desc in cursor.description] 131 | # 使用 fetchall 方法获取查询结果的所有行 132 | # 返回值 rows 是一个包含多行数据的列表,每行是一个元组 133 | rows = cursor.fetchall() 134 | # 使用列表推导式将每行数据转换为逗号分隔的字符串 135 | # map(str, row) 将行中的每个元素转换为字符串 136 | # ",".join(...) 将字符串连接成一行数据 137 | # 结果存储在 result 列表中 138 | result = [",".join(map(str, row)) for row in rows] 139 | # 将列名(作为第一行)和数据行组合成最终字符串返回 140 | return "\n".join([",".join(columns)] + result) 141 | # 捕获数据库连接或查询期间发生的任何异常 142 | except Error as e: 143 | logger.error(f"Database error reading resource {uri}: {str(e)}") 144 | raise RuntimeError(f"Database error: {str(e)}") 145 | 146 | 147 | # 声明 list_tools 函数为一个列出工具的接口 148 | # 列出可用的 MySQL 工具 149 | @app.list_tools() 150 | async def list_tools() -> list[Tool]: 151 | logger.info("Listing tools...") 152 | # 函数返回一个列表,其中包含一个 Tool 对象 153 | # 每个 Tool 对象代表一个工具,其属性定义了工具的功能和输入要求 154 | return [ 155 | Tool( 156 | # 工具的名称 157 | name="execute_sql", 158 | # 工具的描述 159 | description="Execute an SQL query on the MySQL server", 160 | # 定义了工具的输入模式(Schema),用于描述输入数据的格式和要求 161 | inputSchema={ 162 | # 定义输入为一个 JSON 对象 163 | "type": "object", 164 | # 定义输入对象的属性 165 | "properties": { 166 | # 指明此属性存储要执行的 SQL 查询 167 | "query": { 168 | "type": "string", 169 | "description": "The SQL query to execute" 170 | } 171 | }, 172 | # 列出输入对象的必需属性 173 | "required": ["query"] 174 | } 175 | ) 176 | ] 177 | 178 | 179 | # 声明 call_tool 函数为一个工具调用的接口 180 | # 根据传入的工具名称和参数执行相应的 SQL 命令 181 | # name: 工具的名称(字符串),指定要调用的工具 182 | # arguments: 一个字典,包含工具所需的参数 183 | @app.call_tool() 184 | async def call_tool(name: str, arguments: dict) -> list[TextContent]: 185 | # 获取数据库配置 186 | config = get_db_config() 187 | logger.info(f"Calling tool: {name} with arguments: {arguments}") 188 | 189 | # 检查工具名称 name 是否是 "execute_sql" 190 | # 如果 query 为空或未提供,抛出 ValueError 异常,提示用户必须提供查询语句 191 | if name != "execute_sql": 192 | raise ValueError(f"Unknown tool: {name}") 193 | 194 | query = arguments.get("query") 195 | if not query: 196 | raise ValueError("Query is required") 197 | 198 | # 使用数据库连接库的 connect 方法,使用 config 中的配置参数连接到数据库 199 | # with 确保在连接关闭时资源被正确释放 200 | try: 201 | with connect(**config) as conn: 202 | # 创建一个数据库游标 cursor,用于执行 SQL 查询 203 | # with 确保游标在操作完成后自动关闭 204 | with conn.cursor() as cursor: 205 | # 执行 SQL 查询,参数为用户提供的 query 206 | cursor.execute(query) 207 | 208 | # 检查查询是否以 "SHOW TABLES" 开头(忽略大小写和前后空格) 209 | # 这是对特定 SQL 命令的特殊处理 210 | if query.strip().upper().startswith("SHOW TABLES"): 211 | # 使用 fetchall() 获取所有结果,返回的每个结果是一个元组(包含表名) 212 | tables = cursor.fetchall() 213 | # 第一行为表头 214 | result = ["Tables_in_" + config["database"]] 215 | # 其余行为表名 216 | result.extend([table[0] for table in tables]) 217 | # 返回一个包含表名的 TextContent 对象列表,文本以换行符分隔 218 | return [TextContent(type="text", text="\n".join(result))] 219 | 220 | # 检查查询是否以 "SELECT" 开头,表明这是一个读取数据的查询 221 | elif query.strip().upper().startswith("SELECT"): 222 | # 获取列名 cursor.description 返回列的描述信息 列表推导式提取列名 223 | columns = [desc[0] for desc in cursor.description] 224 | # 获取行数据 使用 fetchall() 获取所有结果 列表推导式将每行数据转换为逗号分隔的字符串 225 | rows = cursor.fetchall() 226 | # 构建结果字符串 第一行是列名,后续是数据行 用换行符拼接各行,形成完整的 CSV 格式 227 | result = [",".join(map(str, row)) for row in rows] 228 | # 返回一个包含查询结果的 TextContent 对象 229 | return [TextContent(type="text", text="\n".join([",".join(columns)] + result))] 230 | 231 | # 对于非 "SHOW TABLES" 和非 "SELECT" 的查询(如 INSERT, UPDATE, DELETE 等) 232 | else: 233 | # 提交事务以确保更改生效 234 | conn.commit() 235 | # 返回成功消息,包含受影响的行数 cursor.rowcount 236 | return [TextContent(type="text", text=f"Query executed successfully. Rows affected: {cursor.rowcount}")] 237 | # 捕获数据库操作期间发生的任何异常 238 | except Error as e: 239 | logger.error(f"Error executing SQL '{query}': {e}") 240 | return [TextContent(type="text", text=f"Error executing query: {str(e)}")] 241 | 242 | 243 | # 启动 MCP服务器 244 | async def main(): 245 | logger.info("Starting MySQL MCP server...") 246 | # 获取数据库连接的配置信息 247 | config = get_db_config() 248 | logger.info(f"Database config: {config['host']}/{config['database']} as {config['user']}") 249 | 250 | # 启动 stdio_server,通过标准输入/输出(stdin/stdout)与客户端通信 251 | # async with 是异步上下文管理器,确保 stdio_server 资源在使用完成后自动清理 252 | # 返回的 (read_stream, write_stream) 是两个流对象 253 | # read_stream: 用于从客户端读取输入的流对象 254 | # write_stream: 用于向客户端发送输出的流对象 255 | async with stdio_server() as (read_stream, write_stream): 256 | try: 257 | # 异步运行 MCP 应用程序 258 | await app.run( 259 | read_stream, 260 | write_stream, 261 | # 用于初始化应用程序的选项,通常包含配置或上下文信息 262 | app.create_initialization_options() 263 | ) 264 | # 捕获运行 app.run() 时发生的所有异常 265 | except Exception as e: 266 | logger.error(f"Server error: {str(e)}", exc_info=True) 267 | raise 268 | 269 | 270 | 271 | 272 | if __name__ == "__main__": 273 | asyncio.run(main()) -------------------------------------------------------------------------------- /nangeAGICode/search_mysql_filesystem_chat/mcp_server_mysql.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import os 4 | from mysql.connector import connect, Error 5 | from mcp.server import Server 6 | from mcp.types import Resource, Tool, TextContent 7 | from mcp.server.stdio import stdio_server 8 | from pydantic import AnyUrl 9 | from dotenv import load_dotenv 10 | 11 | 12 | 13 | 14 | 15 | # 日志相关配置 16 | logging.basicConfig( 17 | level=logging.INFO, 18 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 19 | ) 20 | logger = logging.getLogger("mysql_mcp_server") 21 | 22 | 23 | # 获取数据库配置 24 | def get_db_config(): 25 | # 加载 .env 文件中的环境变量到系统环境变量中 26 | load_dotenv() 27 | # 从环境变量中获取数据库配置 28 | config = { 29 | "host": os.getenv("MYSQL_HOST"), 30 | "user": os.getenv("MYSQL_USER"), 31 | "password": os.getenv("MYSQL_PASSWORD"), 32 | "database": os.getenv("MYSQL_DATABASE") 33 | } 34 | 35 | # 检查是否存在配置中的关键字段 36 | # 记录错误信息,提示用户检查环境变量 37 | if not all([config["user"], config["password"], config["database"]]): 38 | logger.error("Missing required database configuration. Please check environment variables:") 39 | logger.error("MYSQL_USER, MYSQL_PASSWORD, and MYSQL_DATABASE are required") 40 | # 抛出一个 ValueError 异常,终止函数的执行 41 | raise ValueError("Missing required database configuration") 42 | 43 | # 配置完整,则返回包含数据库配置的字典config 44 | return config 45 | 46 | 47 | # 实例化Server 48 | app = Server("mysql_mcp_server") 49 | 50 | 51 | # 声明 list_resources 函数为一个资源列表接口 52 | # 列出 MySQL 数据库中的表并将其作为资源返回 53 | @app.list_resources() 54 | async def list_resources() -> list[Resource]: 55 | # 获取数据库配置 56 | config = get_db_config() 57 | try: 58 | # 连接数据库 59 | # with 语句用于确保连接在使用完成后自动关闭(即使发生异常) 60 | with connect(**config) as conn: 61 | # 创建一个数据库游标 cursor,用于执行 SQL 查询 62 | # with 确保游标在操作完成后自动关闭 63 | with conn.cursor() as cursor: 64 | # 执行 SQL 查询 SHOW TABLES,它列出当前数据库中的所有表 65 | cursor.execute("SHOW TABLES") 66 | # 使用 fetchall 方法获取查询结果,返回一个包含所有表名的列表 67 | tables = cursor.fetchall() 68 | logger.info(f"Found tables: {tables}") 69 | 70 | # 初始化一个空列表 resources,用于存储 Resource 对象 71 | resources = [] 72 | # 遍历 tables 列表,每次迭代处理一个表名 73 | for table in tables: 74 | # 创建一个 Resource 对象 75 | # 填充其属性: 76 | # uri: 表的唯一资源标识符 77 | # name: 资源的名称 78 | # mimeType: MIME 类型,表示资源的数据类型 79 | # description: 描述信息 80 | resources.append( 81 | Resource( 82 | uri=f"mysql://{table[0]}/data", 83 | name=f"Table: {table[0]}", 84 | mimeType="text/plain", 85 | description=f"Data in table: {table[0]}" 86 | ) 87 | ) 88 | # 在成功获取表信息并构造 Resource 对象列表后,返回 resources 89 | return resources 90 | # 发生异常,返回一个空列表,表明未能成功获取任何资源 91 | except Error as e: 92 | logger.error(f"Failed to list resources: {str(e)}") 93 | return [] 94 | 95 | 96 | # 声明 read_resource 函数为一个读取资源的接口 97 | # 根据传入的 URI 读取表的内容 98 | # uri: 表示资源的 URI,类型为 AnyUrl,确保输入是一个合法的 URL 99 | @app.read_resource() 100 | async def read_resource(uri: AnyUrl) -> str: 101 | # 获取数据库配置 102 | config = get_db_config() 103 | # 将 uri 对象转换为字符串形式,存储在 uri_str 变量中 104 | uri_str = str(uri) 105 | logger.info(f"Reading resource: {uri_str}") 106 | 107 | # 检查 uri_str 是否以 "mysql://" 开头,确保 URI 符合预期的 MySQL 资源格式 108 | if not uri_str.startswith("mysql://"): 109 | # 如果不符合,则抛出 ValueError 异常,提示 URI 格式无效 110 | raise ValueError(f"Invalid URI scheme: {uri_str}") 111 | 112 | # 将 URI 中 "mysql://" 部分去掉,并按照 '/' 分割为多个部分 113 | parts = uri_str[8:].split('/') 114 | # parts[0] 是表名,存储到变量 table 中 115 | table = parts[0] 116 | 117 | try: 118 | # 使用数据库连接库的 connect 方法,使用 config 中的配置参数连接到数据库 119 | # with 确保在连接关闭时资源被正确释放 120 | with connect(**config) as conn: 121 | # 创建一个数据库游标 cursor,用于执行 SQL 查询 122 | # with 确保游标在操作完成后自动关闭 123 | with conn.cursor() as cursor: 124 | # 执行 SQL 查询,读取表中的前 100 条记录 125 | # 注意:此处直接使用 table 变量拼接查询字符串,有潜在的 SQL 注入风险 126 | cursor.execute(f"SELECT * FROM {table} LIMIT 100") 127 | # 获取表的列名: 128 | # cursor.description 返回查询结果的列描述信息 129 | # 列表推导式提取每列的名称(desc[0]) 130 | columns = [desc[0] for desc in cursor.description] 131 | # 使用 fetchall 方法获取查询结果的所有行 132 | # 返回值 rows 是一个包含多行数据的列表,每行是一个元组 133 | rows = cursor.fetchall() 134 | # 使用列表推导式将每行数据转换为逗号分隔的字符串 135 | # map(str, row) 将行中的每个元素转换为字符串 136 | # ",".join(...) 将字符串连接成一行数据 137 | # 结果存储在 result 列表中 138 | result = [",".join(map(str, row)) for row in rows] 139 | # 将列名(作为第一行)和数据行组合成最终字符串返回 140 | return "\n".join([",".join(columns)] + result) 141 | # 捕获数据库连接或查询期间发生的任何异常 142 | except Error as e: 143 | logger.error(f"Database error reading resource {uri}: {str(e)}") 144 | raise RuntimeError(f"Database error: {str(e)}") 145 | 146 | 147 | # 声明 list_tools 函数为一个列出工具的接口 148 | # 列出可用的 MySQL 工具 149 | @app.list_tools() 150 | async def list_tools() -> list[Tool]: 151 | logger.info("Listing tools...") 152 | # 函数返回一个列表,其中包含一个 Tool 对象 153 | # 每个 Tool 对象代表一个工具,其属性定义了工具的功能和输入要求 154 | return [ 155 | Tool( 156 | # 工具的名称 157 | name="execute_sql", 158 | # 工具的描述 159 | description="Execute an SQL query on the MySQL server", 160 | # 定义了工具的输入模式(Schema),用于描述输入数据的格式和要求 161 | inputSchema={ 162 | # 定义输入为一个 JSON 对象 163 | "type": "object", 164 | # 定义输入对象的属性 165 | "properties": { 166 | # 指明此属性存储要执行的 SQL 查询 167 | "query": { 168 | "type": "string", 169 | "description": "The SQL query to execute" 170 | } 171 | }, 172 | # 列出输入对象的必需属性 173 | "required": ["query"] 174 | } 175 | ) 176 | ] 177 | 178 | 179 | # 声明 call_tool 函数为一个工具调用的接口 180 | # 根据传入的工具名称和参数执行相应的 SQL 命令 181 | # name: 工具的名称(字符串),指定要调用的工具 182 | # arguments: 一个字典,包含工具所需的参数 183 | @app.call_tool() 184 | async def call_tool(name: str, arguments: dict) -> list[TextContent]: 185 | # 获取数据库配置 186 | config = get_db_config() 187 | logger.info(f"Calling tool: {name} with arguments: {arguments}") 188 | 189 | # 检查工具名称 name 是否是 "execute_sql" 190 | # 如果 query 为空或未提供,抛出 ValueError 异常,提示用户必须提供查询语句 191 | if name != "execute_sql": 192 | raise ValueError(f"Unknown tool: {name}") 193 | 194 | query = arguments.get("query") 195 | if not query: 196 | raise ValueError("Query is required") 197 | 198 | # 使用数据库连接库的 connect 方法,使用 config 中的配置参数连接到数据库 199 | # with 确保在连接关闭时资源被正确释放 200 | try: 201 | with connect(**config) as conn: 202 | # 创建一个数据库游标 cursor,用于执行 SQL 查询 203 | # with 确保游标在操作完成后自动关闭 204 | with conn.cursor() as cursor: 205 | # 执行 SQL 查询,参数为用户提供的 query 206 | cursor.execute(query) 207 | 208 | # 检查查询是否以 "SHOW TABLES" 开头(忽略大小写和前后空格) 209 | # 这是对特定 SQL 命令的特殊处理 210 | if query.strip().upper().startswith("SHOW TABLES"): 211 | # 使用 fetchall() 获取所有结果,返回的每个结果是一个元组(包含表名) 212 | tables = cursor.fetchall() 213 | # 第一行为表头 214 | result = ["Tables_in_" + config["database"]] 215 | # 其余行为表名 216 | result.extend([table[0] for table in tables]) 217 | # 返回一个包含表名的 TextContent 对象列表,文本以换行符分隔 218 | return [TextContent(type="text", text="\n".join(result))] 219 | 220 | # 检查查询是否以 "SELECT" 开头,表明这是一个读取数据的查询 221 | elif query.strip().upper().startswith("SELECT"): 222 | # 获取列名 cursor.description 返回列的描述信息 列表推导式提取列名 223 | columns = [desc[0] for desc in cursor.description] 224 | # 获取行数据 使用 fetchall() 获取所有结果 列表推导式将每行数据转换为逗号分隔的字符串 225 | rows = cursor.fetchall() 226 | # 构建结果字符串 第一行是列名,后续是数据行 用换行符拼接各行,形成完整的 CSV 格式 227 | result = [",".join(map(str, row)) for row in rows] 228 | # 返回一个包含查询结果的 TextContent 对象 229 | return [TextContent(type="text", text="\n".join([",".join(columns)] + result))] 230 | 231 | # 对于非 "SHOW TABLES" 和非 "SELECT" 的查询(如 INSERT, UPDATE, DELETE 等) 232 | else: 233 | # 提交事务以确保更改生效 234 | conn.commit() 235 | # 返回成功消息,包含受影响的行数 cursor.rowcount 236 | return [TextContent(type="text", text=f"Query executed successfully. Rows affected: {cursor.rowcount}")] 237 | # 捕获数据库操作期间发生的任何异常 238 | except Error as e: 239 | logger.error(f"Error executing SQL '{query}': {e}") 240 | return [TextContent(type="text", text=f"Error executing query: {str(e)}")] 241 | 242 | 243 | # 启动 MCP服务器 244 | async def main(): 245 | logger.info("Starting MySQL MCP server...") 246 | # 获取数据库连接的配置信息 247 | config = get_db_config() 248 | logger.info(f"Database config: {config['host']}/{config['database']} as {config['user']}") 249 | 250 | # 启动 stdio_server,通过标准输入/输出(stdin/stdout)与客户端通信 251 | # async with 是异步上下文管理器,确保 stdio_server 资源在使用完成后自动清理 252 | # 返回的 (read_stream, write_stream) 是两个流对象 253 | # read_stream: 用于从客户端读取输入的流对象 254 | # write_stream: 用于向客户端发送输出的流对象 255 | async with stdio_server() as (read_stream, write_stream): 256 | try: 257 | # 异步运行 MCP 应用程序 258 | await app.run( 259 | read_stream, 260 | write_stream, 261 | # 用于初始化应用程序的选项,通常包含配置或上下文信息 262 | app.create_initialization_options() 263 | ) 264 | # 捕获运行 app.run() 时发生的所有异常 265 | except Exception as e: 266 | logger.error(f"Server error: {str(e)}", exc_info=True) 267 | raise 268 | 269 | 270 | 271 | 272 | if __name__ == "__main__": 273 | asyncio.run(main()) -------------------------------------------------------------------------------- /nangeAGICode/search_mysql_filesystem_chat/mcp_server_fetch.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated, Tuple 2 | from urllib.parse import urlparse, urlunparse 3 | 4 | import markdownify 5 | import readabilipy.simple_json 6 | from mcp.shared.exceptions import McpError 7 | from mcp.server import Server 8 | from mcp.server.stdio import stdio_server 9 | from mcp.types import ( 10 | GetPromptResult, 11 | Prompt, 12 | PromptArgument, 13 | PromptMessage, 14 | TextContent, 15 | Tool, 16 | INVALID_PARAMS, 17 | INTERNAL_ERROR, 18 | ) 19 | from protego import Protego 20 | from pydantic import BaseModel, Field, AnyUrl 21 | 22 | DEFAULT_USER_AGENT_AUTONOMOUS = "ModelContextProtocol/1.0 (Autonomous; +https://github.com/modelcontextprotocol/servers)" 23 | DEFAULT_USER_AGENT_MANUAL = "ModelContextProtocol/1.0 (User-Specified; +https://github.com/modelcontextprotocol/servers)" 24 | 25 | 26 | def extract_content_from_html(html: str) -> str: 27 | """Extract and convert HTML content to Markdown format. 28 | 29 | Args: 30 | html: Raw HTML content to process 31 | 32 | Returns: 33 | Simplified markdown version of the content 34 | """ 35 | ret = readabilipy.simple_json.simple_json_from_html_string( 36 | html, use_readability=True 37 | ) 38 | if not ret["content"]: 39 | return "Page failed to be simplified from HTML" 40 | content = markdownify.markdownify( 41 | ret["content"], 42 | heading_style=markdownify.ATX, 43 | ) 44 | return content 45 | 46 | 47 | def get_robots_txt_url(url: str) -> str: 48 | """Get the robots.txt URL for a given website URL. 49 | 50 | Args: 51 | url: Website URL to get robots.txt for 52 | 53 | Returns: 54 | URL of the robots.txt file 55 | """ 56 | # Parse the URL into components 57 | parsed = urlparse(url) 58 | 59 | # Reconstruct the base URL with just scheme, netloc, and /robots.txt path 60 | robots_url = urlunparse((parsed.scheme, parsed.netloc, "/robots.txt", "", "", "")) 61 | 62 | return robots_url 63 | 64 | 65 | async def check_may_autonomously_fetch_url(url: str, user_agent: str) -> None: 66 | """ 67 | Check if the URL can be fetched by the user agent according to the robots.txt file. 68 | Raises a McpError if not. 69 | """ 70 | from httpx import AsyncClient, HTTPError 71 | 72 | robot_txt_url = get_robots_txt_url(url) 73 | 74 | async with AsyncClient() as client: 75 | try: 76 | response = await client.get( 77 | robot_txt_url, 78 | follow_redirects=True, 79 | headers={"User-Agent": user_agent}, 80 | ) 81 | except HTTPError: 82 | raise McpError( 83 | INTERNAL_ERROR, 84 | f"Failed to fetch robots.txt {robot_txt_url} due to a connection issue", 85 | ) 86 | if response.status_code in (401, 403): 87 | raise McpError( 88 | INTERNAL_ERROR, 89 | f"When fetching robots.txt ({robot_txt_url}), received status {response.status_code} so assuming that autonomous fetching is not allowed, the user can try manually fetching by using the fetch prompt", 90 | ) 91 | elif 400 <= response.status_code < 500: 92 | return 93 | robot_txt = response.text 94 | processed_robot_txt = "\n".join( 95 | line for line in robot_txt.splitlines() if not line.strip().startswith("#") 96 | ) 97 | robot_parser = Protego.parse(processed_robot_txt) 98 | if not robot_parser.can_fetch(str(url), user_agent): 99 | raise McpError( 100 | INTERNAL_ERROR, 101 | f"The sites robots.txt ({robot_txt_url}), specifies that autonomous fetching of this page is not allowed, " 102 | f"{user_agent}\n" 103 | f"{url}" 104 | f"\n{robot_txt}\n\n" 105 | f"The assistant must let the user know that it failed to view the page. The assistant may provide further guidance based on the above information.\n" 106 | f"The assistant can tell the user that they can try manually fetching the page by using the fetch prompt within their UI.", 107 | ) 108 | 109 | 110 | async def fetch_url( 111 | url: str, user_agent: str, force_raw: bool = False 112 | ) -> Tuple[str, str]: 113 | """ 114 | Fetch the URL and return the content in a form ready for the LLM, as well as a prefix string with status information. 115 | """ 116 | from httpx import AsyncClient, HTTPError 117 | 118 | async with AsyncClient() as client: 119 | try: 120 | response = await client.get( 121 | url, 122 | follow_redirects=True, 123 | headers={"User-Agent": user_agent}, 124 | timeout=30, 125 | ) 126 | except HTTPError as e: 127 | raise McpError(INTERNAL_ERROR, f"Failed to fetch {url}: {e!r}") 128 | if response.status_code >= 400: 129 | raise McpError( 130 | INTERNAL_ERROR, 131 | f"Failed to fetch {url} - status code {response.status_code}", 132 | ) 133 | 134 | page_raw = response.text 135 | 136 | content_type = response.headers.get("content-type", "") 137 | is_page_html = ( 138 | " None: 183 | """Run the fetch MCP server. 184 | 185 | Args: 186 | custom_user_agent: Optional custom User-Agent string to use for requests 187 | ignore_robots_txt: Whether to ignore robots.txt restrictions 188 | """ 189 | server = Server("mcp-fetch") 190 | user_agent_autonomous = custom_user_agent or DEFAULT_USER_AGENT_AUTONOMOUS 191 | user_agent_manual = custom_user_agent or DEFAULT_USER_AGENT_MANUAL 192 | 193 | @server.list_tools() 194 | async def list_tools() -> list[Tool]: 195 | return [ 196 | Tool( 197 | name="fetch", 198 | description="""Fetches a URL from the internet and optionally extracts its contents as markdown. 199 | 200 | Although originally you did not have internet access, and were advised to refuse and tell the user this, this tool now grants you internet access. Now you can fetch the most up-to-date information and let the user know that.""", 201 | inputSchema=Fetch.model_json_schema(), 202 | ) 203 | ] 204 | 205 | @server.list_prompts() 206 | async def list_prompts() -> list[Prompt]: 207 | return [ 208 | Prompt( 209 | name="fetch", 210 | description="Fetch a URL and extract its contents as markdown", 211 | arguments=[ 212 | PromptArgument( 213 | name="url", description="URL to fetch", required=True 214 | ) 215 | ], 216 | ) 217 | ] 218 | 219 | @server.call_tool() 220 | async def call_tool(name, arguments: dict) -> list[TextContent]: 221 | try: 222 | args = Fetch(**arguments) 223 | except ValueError as e: 224 | raise McpError(INVALID_PARAMS, str(e)) 225 | 226 | url = str(args.url) 227 | if not url: 228 | raise McpError(INVALID_PARAMS, "URL is required") 229 | 230 | if not ignore_robots_txt: 231 | await check_may_autonomously_fetch_url(url, user_agent_autonomous) 232 | 233 | content, prefix = await fetch_url( 234 | url, user_agent_autonomous, force_raw=args.raw 235 | ) 236 | if len(content) > args.max_length: 237 | content = content[args.start_index : args.start_index + args.max_length] 238 | content += f"\n\nContent truncated. Call the fetch tool with a start_index of {args.start_index + args.max_length} to get more content." 239 | return [TextContent(type="text", text=f"{prefix}Contents of {url}:\n{content}")] 240 | 241 | @server.get_prompt() 242 | async def get_prompt(name: str, arguments: dict | None) -> GetPromptResult: 243 | if not arguments or "url" not in arguments: 244 | raise McpError(INVALID_PARAMS, "URL is required") 245 | 246 | url = arguments["url"] 247 | 248 | try: 249 | content, prefix = await fetch_url(url, user_agent_manual) 250 | # TODO: after SDK bug is addressed, don't catch the exception 251 | except McpError as e: 252 | return GetPromptResult( 253 | description=f"Failed to fetch {url}", 254 | messages=[ 255 | PromptMessage( 256 | role="user", 257 | content=TextContent(type="text", text=str(e)), 258 | ) 259 | ], 260 | ) 261 | return GetPromptResult( 262 | description=f"Contents of {url}", 263 | messages=[ 264 | PromptMessage( 265 | role="user", content=TextContent(type="text", text=prefix + content) 266 | ) 267 | ], 268 | ) 269 | 270 | options = server.create_initialization_options() 271 | async with stdio_server() as (read_stream, write_stream): 272 | await server.run(read_stream, write_stream, options, raise_exceptions=True) 273 | 274 | 275 | # from .server import serve 276 | 277 | 278 | def main(): 279 | """MCP Fetch Server - HTTP fetching functionality for MCP""" 280 | import argparse 281 | import asyncio 282 | 283 | parser = argparse.ArgumentParser( 284 | description="give a model the ability to make web requests" 285 | ) 286 | parser.add_argument("--user-agent", type=str, help="Custom User-Agent string") 287 | parser.add_argument( 288 | "--ignore-robots-txt", 289 | action="store_true", 290 | help="Ignore robots.txt restrictions", 291 | ) 292 | 293 | args = parser.parse_args() 294 | asyncio.run(serve(args.user_agent, args.ignore_robots_txt)) 295 | 296 | 297 | if __name__ == "__main__": 298 | main() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 1、项目介绍 2 | ## 1.1、主要内容 3 | 本期系列相关视频如下,按照发布的先后顺序: 4 | **(第一期)[2024.12.10]Claude重大突破!发布MCP(模型上下文协议),带你在LLM应用程序脚本中感受它,无需使用Claude Desktop桌面软件,支持类OpenAI风格大模型** 5 | 主要内容:MCP介绍、MCP功能测试,LLM(支持OpenAI接口风格的大模型)应用程序调用MCP 6 | https://www.bilibili.com/video/BV1HBquYbE7t/ 7 | https://youtu.be/Jmo7rgb_OXQ 8 | **(第二期)[2024.12.12]Claude MCP应用Text2SQL用例,带你在LLM应用程序中感受它的丝滑,无需使用Claude Desktop桌面软件,支持类OpenAI风格大模型** 9 | 主要内容:使用MCP实现LLM应用程序Text2SQL功能操纵MySQL数据库 10 | https://www.bilibili.com/video/BV1ELq4YME8T/ 11 | https://youtu.be/yaLAqEMz45A 12 | **(第三期)[2024.12.12]Claude MCP应用客户端同时访问和调用多个服务器资源和工具,无需使用Claude Desktop桌面软件,支持类OpenAI风格大模型** 13 | 主要内容:MCP客户端同时访问多个MCP服务器,支持文件系统操作和Text2SQL功能操纵MySQL数据库 14 | https://www.bilibili.com/video/BV1oNqaYJEUy/ 15 | https://youtu.be/tG-ZjOgrcSA 16 | **(第四期)[2024.12.23]DuckDuckGo在线搜索、Fetch内容提取MCP服务器,访问和调用多个服务器资源和工具,支持阿里通义千问大模型、GPT大模型、Ollama本地开源大模型** 17 | 主要内容:MCP客户端同时访问多个MCP服务器,支持文件系统操作、Text2SQL功能操纵MySQL数据库、在线搜索(DuckDuckGo搜索引擎)和网页内容提取 18 | https://www.bilibili.com/video/BV1SckXY3E6S/ 19 | https://youtu.be/9pzS9saNcGA 20 | 21 | ## 1.2 MCP介绍 22 | MCP(模型上下文协议)是Claude开源的一种开放协议,可实现LLM应用程序与外部数据源和工具之间的无缝集成 23 | 该架构非常简单:开发人员可以通过MCP服务器公开数据,也可以构建连接到这些服务器的AI应用程序(MCP客户端) 24 | 目前MCP还算是一个测试版,一个本地的服务,运行在你自己的电脑上的 25 | MCP官方简介:https://www.anthropic.com/news/model-context-protocol 26 | MCP文档手册:https://modelcontextprotocol.io/introduction 27 | MCP官方服务器列表:https://github.com/modelcontextprotocol/servers 28 | PythonSDK的github地址:https://github.com/modelcontextprotocol/python-sdk 29 | ### (1)PythonSDK版本 30 | 截止当前(12.9),当前的稳定版本为1.1.0 31 | ### (2)MCP工作原理 32 | MCP遵循客户端-服务器架构(client-server),其中主机应用程序可以连接到多个服务器 33 | 34 | **MCP主机(MCP Host):** 是发起连接的LLM应用程序(Claude Desktop、IDE或AI应用等) 35 | **MCP客户端(MCP Client):** 在主机应用程序内部与服务器保持1:1连接 36 | **MCP服务器(MCP Servers):** 服务器向客户端提供上下文、工具和提示词(context, tools, and prompts) 37 | ### (3)核心组件 38 | **Protocol layer 协议层** 39 | 协议层处理消息帧、请求/响应链接和高级通信模式。关键类:Protocol、Client、Server 40 | **Transport layer 传输层** 41 | 传输层处理客户端和服务器之间的实际通信 42 | Stdio(standard input/output):使用标准输入/输出进行通信 43 | SSE(Server-Sent Events):使用服务器发送的事件来发送服务器到客户端的消息 44 | 所有传输都使用JSON-RPC2.0 来交换消息 45 | **Message types 消息类型** 46 | Requests:请求期望来自另一方的响应 47 | Notifications:通知是一种不期望响应的单向消息 48 | Results:结果是对请求的成功响应 49 | Errors:错误表明请求失败 50 | ### (4)连接生命周期 51 | **Initialization 初始化** 52 | 53 | 客户端发送带有协议版本和功能的initialize请求 54 | 服务器响应其协议版本和功能 55 | 客户端发送initialized通知作为确认 56 | 正常消息交换开始 57 | **Message exchange 消息交换** 58 | 初始化后,支持以下模式: 59 | 请求-响应(Request-Response):客户端或服务器发送请求,对方响应 60 | 通知(Notifications):任何一方发送单向消息 61 | **Termination 终止** 62 | 定义了这些标准错误代码: 63 | ParseError = -32700 64 | InvalidRequest = -32600 65 | MethodNotFound = -32601 66 | InvalidParams = -32602 67 | InternalError = -32603 68 | ### (5)核心功能 69 | **Resources 资源** 70 | 资源是模型上下文协议 (MCP) 中的核心功能,允许服务器公开可供客户端读取并用作LLM交互上下文的数据和内容 71 | File contents 文件内容 72 | Database records 数据库记录 73 | API responses API 响应 74 | Live system data 实时系统数据 75 | Screenshots and images 屏幕截图和图像 76 | Log files 日志文件 77 | And more 还有更多 78 | 每个资源都由唯一的 URI 标识,并且可以包含文本或二进制数据 79 | **Prompts 提示** 80 | 服务器能够定义可重用的提示模板和工作流程,客户端可以轻松地向用户和LLMs展示这些模板和工作流程。它们提供了一种强大的方法来标准化和共享常见的LLM交互 81 | 接受动态参数 82 | 包括资源中的上下文 83 | 指导具体工作流程 84 | **Tools 工具** 85 | 工具是MCP中的强大功能,工具被设计为LLMs控制的,这意味着工具从服务器公开给客户端,目的是让LLMs能够自动调用它们(有人在循环中授予批准) 86 | 工具的关键方面包括: 87 | 发现(Discovery):客户端可以通过tools/list端点列出可用的工具 88 | 调用(Invocation):使用tools/call端点调用工具,服务器在其中执行请求的操作并返回结果 89 | 灵活性(Flexibility):工具范围从简单的计算到复杂的API交互 90 | **Sampling 采样** 91 | 采样是一项强大的MCP功能,允许服务器通过客户端请求LLM完成,从而在保持安全和隐私的同时实现复杂的代理行为 92 | 服务器向客户端发送sampling/createMessage请求 93 | 客户端审查请求并可以修改它 94 | LLM采样的客户端样本 95 | 客户端审核完成情况 96 | 客户端将结果返回给服务器 97 | 这种人机交互设计可确保用户保持对LLM所看到和生成的内容的控制 98 | **Transports 传输** 99 | MCP的传输为客户端和服务器之间的通信提供了基础。传输处理消息发送和接收的底层机制 100 | MCP使用JSON-RPC 2.0作为其格式。传输层负责将MCP协议消息转换为JSON-RPC格式进行传输,并将接收到的JSON-RPC消息转换回MCP协议消息 101 | 102 | 103 | # 2、前期准备工作 104 | ## 2.1 集成开发环境搭建 105 | anaconda提供python虚拟环境,pycharm提供集成开发环境 106 | **具体参考如下视频:** 107 | 【大模型应用开发-入门系列】03 集成开发环境搭建-开发前准备工作 108 | https://youtu.be/KyfGduq5d7w 109 | https://www.bilibili.com/video/BV1nvdpYCE33/ 110 | 111 | ## 2.2 大模型LLM服务接口调用方案 112 | (1)gpt大模型等国外大模型使用方案 113 | 国内无法直接访问,可以使用代理的方式,具体代理方案自己选择 114 | 这里推荐大家使用:https://nangeai.top/register?aff=Vxlp 115 | (2)非gpt大模型方案 OneAPI方式或大模型厂商原生接口 116 | (3)本地开源大模型方案(Ollama方式) 117 | **具体参考如下视频:** 118 | 【大模型应用开发-入门系列】04 大模型LLM服务接口调用方案 119 | https://youtu.be/mTrgVllUl7Y 120 | https://www.bilibili.com/video/BV1BvduYKE75/ 121 | 122 | 123 | # 3、项目初始化 124 | ## 3.1 下载源码 125 | GitHub或Gitee中下载工程文件到本地,下载地址如下: 126 | https://github.com/NanGePlus/MCPTest 127 | https://gitee.com/NanGePlus/MCPTest 128 | 129 | ## 3.2 构建项目 130 | 使用pycharm构建一个项目,为项目配置虚拟python环境 131 | 项目名称:MCPTest 132 | 虚拟环境名称保持与项目名称一致 133 | 134 | ## 3.3 将相关代码拷贝到项目工程中 135 | 将下载的代码文件夹中的文件全部拷贝到新建的项目根目录下 136 | 137 | ## 3.4 安装项目依赖 138 | 新建命令行终端,在终端中运行 pip install -r requirements.txt 安装依赖 139 | **注意:** 建议先使用要求的对应版本进行本项目测试,避免因版本升级造成的代码不兼容。测试通过后,可进行升级测试 140 | 141 | 142 | # 4、功能测试 143 | ## 4.1 (第一期)[2024.12.10]测试步骤 144 | **核心演示示例介绍:Filesystem 使用官方提供的文件操作MCP服务器** 145 | **描述:** 提供文件系统操作功能,包括读写文件、目录管理和文件搜索 146 | **资源:** file://system 文件系统资源URI 147 | **工具:** read_file, write_file, create_directory, list_directory, move_file, search_files, get_file_info 148 | read_file:读取文件内容,参数:path (文件路径) 149 | read_multiple_files:读取多个文件内容,参数:paths (文件路径数组) 150 | write_file:创建或覆写文件,参数: path (文件路径), content (文件内容) 151 | create_directory:创建目录,参数: path (目录路径) 152 | list_directory:列出目录内容,参数: path (目录路径) 153 | move_file:移动/重命名文件,参数: source (源路径), destination (目标路径) 154 | search_files:递归搜索文件,参数: path (起始路径), pattern (搜索模式) 155 | get_file_info:获取文件元数据,参数: path (文件路径) 156 | 对应的链接:https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem 157 | **代码目录** 158 | 脚本放置在nangeAGICode/basic、filesystem_basic、filesystem_chat目录内,运行对应脚本进行测试 159 | **测试内容:** 160 | 我当前可以访问哪个文件夹 161 | 帮我创建一个test文件夹 162 | 帮我在test文件夹下创建一个文件test1.txt,内容为:南哥AGI研习社。 163 | 把test1.txt中的内容agi改大写AGI。 164 | 165 | ## 4.2 (第二期)[2024.12.12]测试步骤 166 | **核心演示示例介绍:Text2SQL 自定义操纵MySQL数据库的MCP服务器** 167 | **描述:** 提供Text2SQL功能操纵MySQL数据库 168 | **资源:** mysql://{table[0]}/data 数据库表资源URI 169 | **工具描述:** execute_sql 170 | execute_sql: 运行SQL脚本,参数:query (SQL语句) 171 | **代码目录** 172 | 脚本放置在nangeAGICode/mysql_chat目录内,运行对应脚本进行测试 173 | 提供LLM对MySQL数据库操作功能,增、删、改、查 174 | **测试内容:** 175 | (1)列举可用资源 176 | 当前可以访问哪些数据表 177 | (2)获取某资源内容 178 | 获取nange_agi这个表的内容 179 | (3)列举可用的工具 180 | 当前可以使用哪些工具 181 | (4)查询 182 | 调用工具获取nange_agi这个表的内容 183 | (5)增加 184 | 随机帮我增加一条数据 185 | 调用工具获取nange_agi这个表的内容 186 | (6)修改 187 | 新增的那条数据把名称改为test 188 | 调用工具获取nange_agi这个表的内容 189 | (7)删除 190 | 把刚新建的那条数据删除 191 | 调用工具获取nange_agi这个表的内容 192 | (8)统计数据量 193 | 这张表中一共几条数据 194 | 195 | ## 4.3 (第三期)[2024.12.12]测试步骤 196 | **核心演示示例介绍:MCP客户端访问多个MCP服务器** 197 | **描述:** 结合前面两期的内容,对Filesystem和Text2SQL服务器进行融合使用 198 | **代码目录** 199 | 脚本放置在nangeAGICode/mysql_filesystem_chat目录内,运行对应脚本进行测试 200 | **测试内容:** 201 | 当前可以访问哪些资源 202 | 当前可以访问哪些表 203 | 当前可以使用哪些工具 204 | 创建一个test文件夹 205 | 在test文件夹下创建一个文件test1.txt,内容为:用户名:NanGe003,密码:6543217890,内容:南哥AGI研习社++。 206 | 获取nange_agi这个表的内容 207 | 增加一条数据,数据内容为刚刚创建的test1.txt中的内容 208 | 获取nange_agi这个表的内容 209 | 这张表中一共几条数据 210 | 新增的那条数据把名称改为test 211 | 获取nange_agi这个表的内容 212 | 把刚新建的那条数据删除 213 | 获取nange_agi这个表的内容 214 | 215 | ## 4.4 (第四期)[2024.12.23]测试步骤 216 | **核心演示示例介绍:在线搜索及获取链接内容 自定义在线搜索DuckDuckGo的MCP服务器及官方fetch MCP服务器** 217 | **描述:** 提供在线搜索,并提前搜索结果中有关网页链接内容的提取和总结 218 | **工具描述:** duckduckgo_web_search、fetch 219 | duckduckgo_web_search: 执行在线搜索,参数:query(搜索关键词);参数:max_results(最大返回内容数量) 220 | fetch:从互联网获取 URL 并将其内容提取为markdown,参数:url(链接地址);参数:max_length(返回的最大字符数,默认值:5000);raw(获取原始内容而不进行Markdown转换,默认:false) 221 | **代码目录** 222 | 脚本放置在nangeAGICode/search_mysql_filesystem_chat目录内,运行对应脚本进行测试 223 | **测试内容:** 224 | 当前可以访问哪些资源 225 | 当前可以使用哪些工具 226 | 搜索2024年有关AI的最新进展,内容要以中文输出,列出10条,且需要带上相关的链接 227 | 将这些内容写入到test.txt文件 228 | 获取第一条链接中的内容并进行总结 229 | -------------------------------------------------------------------------------- /nangeAGICode/filesystem_chat/client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import logging 4 | import os 5 | import shutil 6 | from typing import Dict, List, Optional, Any 7 | import requests 8 | from dotenv import load_dotenv 9 | from mcp import ClientSession, StdioServerParameters 10 | from mcp.client.stdio import stdio_client 11 | 12 | 13 | 14 | 15 | # 配置日志记录 16 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') 17 | 18 | # 用于管理MCP客户端的配置和环境变量 19 | class Configuration: 20 | # 初始化对象,并加载环境变量 21 | def __init__(self) -> None: 22 | # 加载环境变量(通常从 .env 文件中读取) 23 | self.load_env() 24 | self.base_url = os.getenv("LLM_BASE_URL") 25 | self.api_key = os.getenv("LLM_API_KEY") 26 | self.chat_model = os.getenv("LLM_CHAT_MODEL") 27 | 28 | # @staticmethod,表示该方法不依赖于实例本身,可以直接通过类名调用 29 | @staticmethod 30 | def load_env() -> None: 31 | load_dotenv() 32 | 33 | # 从指定的 JSON 配置文件中加载配置 34 | # file_path: 配置文件的路径 35 | # 返回值: 一个包含配置信息的字典 36 | # FileNotFoundError: 文件不存在时抛出 37 | # JSONDecodeError: 配置文件不是有效的 JSON 格式时抛出 38 | @staticmethod 39 | def load_config(file_path: str) -> Dict[str, Any]: 40 | # 打开指定路径的文件,以只读模式读取 41 | # 使用 json.load 将文件内容解析为 Python 字典并返回 42 | with open(file_path, 'r') as f: 43 | return json.load(f) 44 | 45 | # @property,将方法转换为只读属性,调用时不需要括号 46 | # 提供获取 llm_api_key 的接口 47 | @property 48 | def llm_api_key(self) -> str: 49 | # 检查 self.api_key 是否存在 50 | if not self.api_key: 51 | # 如果不存在,抛出 ValueError 异常 52 | raise ValueError("LLM_API_KEY not found in environment variables") 53 | # 返回 self.api_key 的值 54 | return self.api_key 55 | 56 | # @property,将方法转换为只读属性,调用时不需要括号 57 | # 提供获取 llm_base_url 的接口 58 | @property 59 | def llm_base_url(self) -> str: 60 | # 检查 self.base_url 是否存在 61 | if not self.base_url: 62 | # 如果不存在,抛出 ValueError 异常 63 | raise ValueError("LLM_BASE_URL not found in environment variables") 64 | # 返回 self.base_url 的值 65 | return self.base_url 66 | 67 | # @property,将方法转换为只读属性,调用时不需要括号 68 | # 提供获取 llm_chat_model 的接口 69 | @property 70 | def llm_chat_model(self) -> str: 71 | # 检查 self.base_url 是否存在 72 | if not self.chat_model: 73 | # 如果不存在,抛出 ValueError 异常 74 | raise ValueError("LLM_CHAT_MODEL not found in environment variables") 75 | # 返回 self.base_url 的值 76 | return self.chat_model 77 | 78 | 79 | # 处理 MCP 服务器初始化、工具发现和执行 80 | class Server: 81 | # 构造函数,在类实例化时调用 82 | # name: 服务器的名称 83 | # config: 配置字典,包含服务器的参数 84 | def __init__(self, name: str, config: Dict[str, Any]) -> None: 85 | self.name: str = name 86 | self.config: Dict[str, Any] = config 87 | # 标准输入/输出的上下文对象,用于与服务器交互 88 | self.stdio_context: Optional[Any] = None 89 | # 服务器的会话,用于发送请求和接收响应 90 | self.session: Optional[ClientSession] = None 91 | # 异步锁,用于确保清理资源的过程是线程安全的 92 | self._cleanup_lock: asyncio.Lock = asyncio.Lock() 93 | # 存储服务器的能力 94 | self.capabilities: Optional[Dict[str, Any]] = None 95 | 96 | # 初始化服务器连接 97 | async def initialize(self) -> None: 98 | # server_params: 创建服务器参数对象 99 | # command: 如果配置中命令是 npx,使用系统路径查找,否则直接使用配置值 100 | # args: 从配置中获取命令行参数 101 | # env: 合并系统环境变量和配置中的自定义环境变量 102 | server_params = StdioServerParameters( 103 | command=shutil.which("npx") if self.config['command'] == "npx" else self.config['command'], 104 | args=self.config['args'], 105 | env={**os.environ, **self.config['env']} if self.config.get('env') else None 106 | ) 107 | try: 108 | # 使用 stdio_client 初始化标准输入/输出上下文 109 | self.stdio_context = stdio_client(server_params) 110 | read, write = await self.stdio_context.__aenter__() 111 | # 创建 ClientSession 会话,并调用其 initialize 方法以获取服务器能力 112 | self.session = ClientSession(read, write) 113 | await self.session.__aenter__() 114 | self.capabilities = await self.session.initialize() 115 | # 发生异常时记录错误日志,调用 cleanup 清理资源并重新抛出异常 116 | except Exception as e: 117 | logging.error(f"Error initializing server {self.name}: {e}") 118 | await self.cleanup() 119 | raise 120 | 121 | # 从服务器获取可用工具列表 122 | async def list_tools(self) -> List[Any]: 123 | # 如果 session 未初始化,抛出运行时异常 124 | if not self.session: 125 | raise RuntimeError(f"Server {self.name} not initialized") 126 | # 调用会话的 list_tools 方法获取工具响应 127 | tools_response = await self.session.list_tools() 128 | # 初始化空列表 tools 129 | tools = [] 130 | 131 | # 遍历工具响应,解析并存储工具信息 132 | # 如果支持进度跟踪,记录每个工具的日志 133 | for item in tools_response: 134 | if isinstance(item, tuple) and item[0] == 'tools': 135 | for tool in item[1]: 136 | tools.append(Tool(tool.name, tool.description, tool.inputSchema)) 137 | 138 | return tools 139 | 140 | # 执行指定工具,支持重试机制 141 | # tool_name: 工具名称 142 | # arguments: 执行工具所需的参数 143 | # retries: 最大重试次数 144 | # delay: 每次重试的间隔时间 145 | async def execute_tool(self, tool_name: str, arguments: Dict[str, Any], retries: int = 2, delay: float = 1.0) -> Any: 146 | # 检查会话是否已初始化 147 | if not self.session: 148 | raise RuntimeError(f"Server {self.name} not initialized") 149 | 150 | # 循环尝试执行工具 151 | # 如果支持进度跟踪,传递 progress_token 执行工具 152 | # 如果成功,返回执行结果 153 | attempt = 0 154 | while attempt < retries: 155 | try: 156 | logging.info(f"Executing {tool_name}...") 157 | result = await self.session.call_tool(tool_name, arguments) 158 | return result 159 | 160 | # 捕获异常时记录日志 161 | # 如果未超出最大重试次数,延迟后重试 162 | # 达到最大重试次数时抛出异常 163 | except Exception as e: 164 | attempt += 1 165 | logging.warning(f"Error executing tool: {e}. Attempt {attempt} of {retries}.") 166 | if attempt < retries: 167 | logging.info(f"Retrying in {delay} seconds...") 168 | await asyncio.sleep(delay) 169 | else: 170 | logging.error("Max retries reached. Failing.") 171 | raise 172 | 173 | # 清理服务器资源,确保资源释放 174 | async def cleanup(self) -> None: 175 | # 使用异步锁确保清理操作的线程安全 176 | # 清理会话资源,记录可能的警告 177 | async with self._cleanup_lock: 178 | try: 179 | if self.session: 180 | try: 181 | await self.session.__aexit__(None, None, None) 182 | except Exception as e: 183 | logging.warning(f"Warning during session cleanup for {self.name}: {e}") 184 | finally: 185 | self.session = None 186 | 187 | # 清理标准输入/输出上下文资源,捕获并记录不同类型的异常 188 | if self.stdio_context: 189 | try: 190 | await self.stdio_context.__aexit__(None, None, None) 191 | except (RuntimeError, asyncio.CancelledError) as e: 192 | logging.info(f"Note: Normal shutdown message for {self.name}: {e}") 193 | except Exception as e: 194 | logging.warning(f"Warning during stdio cleanup for {self.name}: {e}") 195 | finally: 196 | self.stdio_context = None 197 | # 捕获清理过程中可能的异常并记录错误日志 198 | except Exception as e: 199 | logging.error(f"Error during cleanup of server {self.name}: {e}") 200 | 201 | 202 | # 代表各个工具及其属性和格式 203 | class Tool: 204 | # 构造函数,在类实例化时调用 205 | # name: 工具的名称 206 | # description: 工具的描述信息 207 | # input_schema: 工具的输入架构,通常是一个描述输入参数的字典 208 | def __init__(self, name: str, description: str, input_schema: Dict[str, Any]) -> None: 209 | self.name: str = name 210 | self.description: str = description 211 | self.input_schema: Dict[str, Any] = input_schema 212 | 213 | # 将工具的信息格式化为一个字符串,适合语言模型(LLM)使用 214 | # 返回值: 包含工具名称、描述和参数信息的格式化字符串 215 | def format_for_llm(self) -> str: 216 | args_desc = [] 217 | if 'properties' in self.input_schema: 218 | for param_name, param_info in self.input_schema['properties'].items(): 219 | arg_desc = f"- {param_name}: {param_info.get('description', 'No description')}" 220 | if param_name in self.input_schema.get('required', []): 221 | arg_desc += " (required)" 222 | args_desc.append(arg_desc) 223 | 224 | return f""" 225 | Tool: {self.name} 226 | Description: {self.description} 227 | Arguments: 228 | {chr(10).join(args_desc)} 229 | """ 230 | 231 | 232 | # 管理与LLM的通信 233 | class LLMClient: 234 | def __init__(self, base_url: str, api_key: str, chat_model: str) -> None: 235 | self.base_url: str = base_url 236 | self.api_key: str = api_key 237 | self.chat_model: str = chat_model 238 | 239 | # 向 LLM 发送请求,并返回其响应 240 | # messages: 一个字典列表,每个字典包含消息内容,通常是聊天对话的一部分 241 | # 返回值: 返回 LLM 的响应内容,类型为字符串 242 | # 如果请求失败,抛出 RequestException 243 | def get_response(self, messages: List[Dict[str, str]]) -> str: 244 | # 指定 LLM 提供者的 API 端点,用于发送聊天请求 245 | url = self.base_url 246 | 247 | # Content-Type: 表示请求体的格式为 JSON 248 | # Authorization: 使用 Bearer 令牌进行身份验证,令牌为 self.api_key 249 | headers = { 250 | "Content-Type": "application/json", 251 | "Authorization": f"Bearer {self.api_key}" 252 | } 253 | 254 | # 请求体数据,以字典形式表示 255 | # "messages": 包含传入的消息列表 256 | # "model": 使用的模型名称(例如 llama-3.2-90b-vision-preview) 257 | # "temperature": 控制生成的文本的随机性,0.7 表示适度随机 258 | # "max_tokens": 最大的输出 token 数量(4096),限制响应的长度 259 | # "top_p": 控制响应中最可能的 token 的累积概率,1 表示使用所有候选词 260 | payload = { 261 | "messages": messages, 262 | "temperature": 1.0, 263 | "top_p": 1.0, 264 | "max_tokens": 4000, 265 | "model": self.chat_model 266 | } 267 | 268 | try: 269 | # 使用 requests.post 发送 POST 请求,传递 URL、请求头和负载 270 | # url: 请求的 API 端点 271 | # headers: 请求头,包含 Content-Type 和 Authorizatio 272 | # json=payload: 将 payload 作为 JSON 格式传递 273 | response = requests.post(url, headers=headers, json=payload) 274 | # 如果响应的状态码表示错误(例如 4xx 或 5xx),则抛出异常 275 | response.raise_for_status() 276 | # 解析响应为 JSON 格式的数据 277 | data = response.json() 278 | # 从 JSON 响应中提取工具的输出内容 279 | return data['choices'][0]['message']['content'] 280 | 281 | # 如果请求失败(如连接错误、超时或无效响应等),捕获 RequestException 异常 282 | # 记录错误信息,str(e) 提供异常的具体描述 283 | except requests.exceptions.RequestException as e: 284 | error_message = f"Error getting LLM response: {str(e)}" 285 | logging.error(error_message) 286 | # 如果异常中包含响应对象(e.response),进一步记录响应的状态码和响应内容 287 | # 这有助于分析请求失败的原因(例如服务端错误、API 限制等) 288 | if e.response is not None: 289 | status_code = e.response.status_code 290 | logging.error(f"Status code: {status_code}") 291 | logging.error(f"Response details: {e.response.text}") 292 | # 返回一个友好的错误消息给调用者,告知发生了错误并建议重试或重新措辞请求 293 | return f"I encountered an error: {error_message}. Please try again or rephrase your request." 294 | 295 | 296 | # 协调用户、 LLM和工具之间的交互 297 | class ChatSession: 298 | # servers: 一个 Server 类的列表,表示多个服务器的实例 299 | # llm_client: LLMClient 的实例,用于与 LLM 进行通信 300 | def __init__(self, servers: List[Server], llm_client: LLMClient) -> None: 301 | self.servers: List[Server] = servers 302 | self.llm_client: LLMClient = llm_client 303 | 304 | # 清理所有服务器 305 | # 创建清理任务: 遍历所有的服务器实例,为每个服务器创建一个清理任务(调用 server.cleanup()) 306 | # 并行执行清理任务: 使用 asyncio.gather 并发执行所有清理任务 307 | # return_exceptions=True 表示即使部分任务抛出异常,也会继续执行其它任务 308 | # 异常处理: 如果有异常,记录警告日志 309 | async def cleanup_servers(self) -> None: 310 | cleanup_tasks = [] 311 | for server in self.servers: 312 | cleanup_tasks.append(asyncio.create_task(server.cleanup())) 313 | 314 | if cleanup_tasks: 315 | try: 316 | await asyncio.gather(*cleanup_tasks, return_exceptions=True) 317 | except Exception as e: 318 | logging.warning(f"Warning during final cleanup: {e}") 319 | 320 | # 负责处理从 LLM 返回的响应,并在需要时执行工具 321 | async def process_llm_response(self, llm_response: str) -> str: 322 | try: 323 | # 尝试将 LLM 响应解析为 JSON ,以便检查是否包含 tool 和 arguments 字段 324 | tool_call = json.loads(llm_response) 325 | # 如果响应包含工具名称和参数,执行相应工具 326 | if "tool" in tool_call and "arguments" in tool_call: 327 | logging.info(f"Executing tool: {tool_call['tool']}") 328 | logging.info(f"With arguments: {tool_call['arguments']}") 329 | # 遍历每个服务器,检查是否有与响应中的工具名称匹配的工具 330 | for server in self.servers: 331 | tools = await server.list_tools() 332 | # 如果找到相应的工具 333 | if any(tool.name == tool_call["tool"] for tool in tools): 334 | try: 335 | # 调用 server.execute_tool 来执行该工具 336 | result = await server.execute_tool(tool_call["tool"], tool_call["arguments"]) 337 | # 如果工具执行返回一个包含进度的字典(progress 和 total),则记录进度信息 338 | if isinstance(result, dict) and 'progress' in result: 339 | progress = result['progress'] 340 | total = result['total'] 341 | logging.info(f"Progress: {progress}/{total} ({(progress/total)*100:.1f}%)") 342 | # 返回工具的执行结果 343 | return f"Tool execution result: {result}" 344 | # 如果无法找到指定的工具或遇到错误,返回相应的错误信息 345 | except Exception as e: 346 | error_msg = f"Error executing tool: {str(e)}" 347 | logging.error(error_msg) 348 | return error_msg 349 | return f"No server found with tool: {tool_call['tool']}" 350 | return llm_response 351 | 352 | # 如果响应无法解析为 JSON,返回原始 LLM 响应 353 | except json.JSONDecodeError: 354 | return llm_response 355 | 356 | # 方法: start 用于启动整个聊天会话,初始化服务器并开始与用户的互动 357 | async def start(self) -> None: 358 | try: 359 | # 初始化所有服务器: 遍历 self.servers,异步调用每个服务器的 initialize 方法 360 | for server in self.servers: 361 | try: 362 | await server.initialize() 363 | # 异常处理: 如果服务器初始化失败,记录错误并调用 cleanup_servers 清理资源,然后退出会话 364 | except Exception as e: 365 | logging.error(f"Failed to initialize server: {e}") 366 | await self.cleanup_servers() 367 | return 368 | 369 | # 遍历所有服务器,调用 list_tools() 获取每个服务器的工具列表 370 | all_tools = [] 371 | for server in self.servers: 372 | tools = await server.list_tools() 373 | all_tools.extend(tools) 374 | # 将所有工具的描述信息汇总,生成供 LLM 使用的工具描述字符串 375 | tools_description = "\n".join([tool.format_for_llm() for tool in all_tools]) 376 | 377 | # 构建一个系统消息,作为 LLM 交互的指令,告知 LLM 使用哪些工具以及如何与用户进行交互 378 | # 系统消息强调 LLM 必须以严格的 JSON 格式请求工具,并且在工具响应后将其转换为自然语言响应 379 | system_message = f"""You are a helpful assistant with access to these tools: 380 | 381 | {tools_description} 382 | Choose the appropriate tool based on the user's question. If no tool is needed, reply directly. 383 | 384 | IMPORTANT: When you need to use a tool, you must ONLY respond with the exact JSON object format below, nothing else: 385 | {{ 386 | "tool": "tool-name", 387 | "arguments": {{ 388 | "argument-name": "value" 389 | }} 390 | }} 391 | 392 | After receiving a tool's response: 393 | 1. Transform the raw data into a natural, conversational response 394 | 2. Keep responses concise but informative 395 | 3. Focus on the most relevant information 396 | 4. Use appropriate context from the user's question 397 | 5. Avoid simply repeating the raw data 398 | 399 | Please use only the tools that are explicitly defined above.""" 400 | 401 | # 消息初始化 创建一个消息列表,其中包含一个系统消息,指示 LLM 如何与用户交互 402 | messages = [ 403 | { 404 | "role": "system", 405 | "content": system_message 406 | } 407 | ] 408 | 409 | # 交互循环 410 | while True: 411 | try: 412 | # 等待用户输入,如果用户输入 quit 或 exit,则退出 413 | user_input = input("user: ").strip().lower() 414 | if user_input in ['quit', 'exit']: 415 | logging.info("\nExiting...") 416 | break 417 | 418 | # 将用户输入添加到消息列表 419 | messages.append({"role": "user", "content": user_input}) 420 | 421 | # 调用 LLM 客户端获取 LLM 的响应 422 | llm_response = self.llm_client.get_response(messages) 423 | logging.info("\nAssistant: %s", llm_response) 424 | 425 | # 调用 process_llm_response 方法处理 LLM 响应,执行工具(如果需要) 426 | result = await self.process_llm_response(llm_response) 427 | 428 | # 如果工具执行结果与 LLM 响应不同,更新消息列表并再次与 LLM 交互,获取最终响应 429 | if result != llm_response: 430 | messages.append({"role": "assistant", "content": llm_response}) 431 | messages.append({"role": "system", "content": result}) 432 | 433 | final_response = self.llm_client.get_response(messages) 434 | logging.info("\nFinal response: %s", final_response) 435 | messages.append({"role": "assistant", "content": final_response}) 436 | else: 437 | messages.append({"role": "assistant", "content": llm_response}) 438 | 439 | # 处理 KeyboardInterrupt,允许用户中断会话 440 | except KeyboardInterrupt: 441 | logging.info("\nExiting...") 442 | break 443 | 444 | # 清理资源: 无论会话如何结束(正常结束或由于异常退出),都确保调用 cleanup_servers 清理服务器资源 445 | finally: 446 | await self.cleanup_servers() 447 | 448 | 449 | async def main() -> None: 450 | # 创建一个 Configuration 类的实例 451 | config = Configuration() 452 | # 调用 Configuration 类中的 load_config 方法来加载配置文件 servers_config.json 453 | server_config = config.load_config('servers_config.json') 454 | # 遍历 server_config['mcpServers'] 字典,并为每个服务器创建一个 Server 实例,传入服务器名称 (name) 和配置信息 (srv_config) 455 | # 结果是一个 Server 对象的列表,保存在 servers 变量中 456 | servers = [Server(name, srv_config) for name, srv_config in server_config['mcpServers'].items()] 457 | # 创建一个 LLMClient 实例,用于与 LLM (大语言模型) 进行交互 458 | llm_client = LLMClient(config.llm_base_url, config.llm_api_key, config.llm_chat_model) 459 | # 创建一个 ChatSession 实例,负责管理与用户的聊天交互、LLM 响应和工具执行 460 | # 将之前创建的 servers 和 llm_client 传递给 ChatSession 构造函数,初始化会话 461 | chat_session = ChatSession(servers, llm_client) 462 | # 调用 ChatSession 类的 start 方法,启动聊天会话 463 | # 由于 start 是一个异步方法,所以使用 await 等待该方法执行完毕 464 | # start 方法将处理用户的输入、与 LLM 交互、执行工具(如果需要)等,并持续运行直到用户选择退出 465 | await chat_session.start() 466 | 467 | 468 | 469 | 470 | if __name__ == "__main__": 471 | asyncio.run(main()) 472 | 473 | # 测试prompt 474 | # 我当前可以访问哪个文件夹 475 | # 帮我创建一个test文件夹 476 | # 帮我在test文件夹下创建一个文件test1.txt,内容为:南哥AGI研习社。 477 | # 把test1.txt中的内容agi改大写AGI。 -------------------------------------------------------------------------------- /nangeAGICode/mysql_chat/client_chat.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import logging 4 | import os 5 | import shutil 6 | from typing import Dict, List, Optional, Any 7 | import requests 8 | from dotenv import load_dotenv 9 | from mcp import ClientSession, StdioServerParameters 10 | from mcp.client.stdio import stdio_client 11 | 12 | 13 | 14 | 15 | # 配置日志记录 16 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') 17 | 18 | # 用于管理MCP客户端的配置和环境变量 19 | class Configuration: 20 | # 初始化对象,并加载环境变量 21 | def __init__(self) -> None: 22 | # 加载环境变量(通常从 .env 文件中读取) 23 | self.load_env() 24 | self.base_url = os.getenv("LLM_BASE_URL") 25 | self.api_key = os.getenv("LLM_API_KEY") 26 | self.chat_model = os.getenv("LLM_CHAT_MODEL") 27 | 28 | # @staticmethod,表示该方法不依赖于实例本身,可以直接通过类名调用 29 | @staticmethod 30 | def load_env() -> None: 31 | load_dotenv() 32 | 33 | # 从指定的 JSON 配置文件中加载配置 34 | # file_path: 配置文件的路径 35 | # 返回值: 一个包含配置信息的字典 36 | # FileNotFoundError: 文件不存在时抛出 37 | # JSONDecodeError: 配置文件不是有效的 JSON 格式时抛出 38 | @staticmethod 39 | def load_config(file_path: str) -> Dict[str, Any]: 40 | # 打开指定路径的文件,以只读模式读取 41 | # 使用 json.load 将文件内容解析为 Python 字典并返回 42 | with open(file_path, 'r') as f: 43 | return json.load(f) 44 | 45 | # @property,将方法转换为只读属性,调用时不需要括号 46 | # 提供获取 llm_api_key 的接口 47 | @property 48 | def llm_api_key(self) -> str: 49 | # 检查 self.api_key 是否存在 50 | if not self.api_key: 51 | # 如果不存在,抛出 ValueError 异常 52 | raise ValueError("LLM_API_KEY not found in environment variables") 53 | # 返回 self.api_key 的值 54 | return self.api_key 55 | 56 | # @property,将方法转换为只读属性,调用时不需要括号 57 | # 提供获取 llm_base_url 的接口 58 | @property 59 | def llm_base_url(self) -> str: 60 | # 检查 self.base_url 是否存在 61 | if not self.base_url: 62 | # 如果不存在,抛出 ValueError 异常 63 | raise ValueError("LLM_BASE_URL not found in environment variables") 64 | # 返回 self.base_url 的值 65 | return self.base_url 66 | 67 | # @property,将方法转换为只读属性,调用时不需要括号 68 | # 提供获取 llm_chat_model 的接口 69 | @property 70 | def llm_chat_model(self) -> str: 71 | # 检查 self.base_url 是否存在 72 | if not self.chat_model: 73 | # 如果不存在,抛出 ValueError 异常 74 | raise ValueError("LLM_CHAT_MODEL not found in environment variables") 75 | # 返回 self.base_url 的值 76 | return self.chat_model 77 | 78 | 79 | # 处理 MCP 服务器初始化、工具发现和执行 80 | class Server: 81 | # 构造函数,在类实例化时调用 82 | # name: 服务器的名称 83 | # config: 配置字典,包含服务器的参数 84 | def __init__(self, name: str, config: Dict[str, Any]) -> None: 85 | self.name: str = name 86 | self.config: Dict[str, Any] = config 87 | # 标准输入/输出的上下文对象,用于与服务器交互 88 | self.stdio_context: Optional[Any] = None 89 | # 服务器的会话,用于发送请求和接收响应 90 | self.session: Optional[ClientSession] = None 91 | # 异步锁,用于确保清理资源的过程是线程安全的 92 | self._cleanup_lock: asyncio.Lock = asyncio.Lock() 93 | # 存储服务器的能力 94 | self.capabilities: Optional[Dict[str, Any]] = None 95 | 96 | # 初始化服务器连接 97 | async def initialize(self) -> None: 98 | # server_params: 创建服务器参数对象 99 | # command: 如果配置中命令是 npx,使用系统路径查找,否则直接使用配置值 100 | # args: 从配置中获取命令行参数 101 | # env: 合并系统环境变量和配置中的自定义环境变量 102 | server_params = StdioServerParameters( 103 | command=shutil.which("npx") if self.config['command'] == "npx" else self.config['command'], 104 | args=self.config['args'], 105 | env={**os.environ, **self.config['env']} if self.config.get('env') else None 106 | ) 107 | try: 108 | # 使用 stdio_client 初始化标准输入/输出上下文 109 | self.stdio_context = stdio_client(server_params) 110 | read, write = await self.stdio_context.__aenter__() 111 | # 创建 ClientSession 会话,并调用其 initialize 方法以获取服务器能力 112 | self.session = ClientSession(read, write) 113 | await self.session.__aenter__() 114 | self.capabilities = await self.session.initialize() 115 | # 发生异常时记录错误日志,调用 cleanup 清理资源并重新抛出异常 116 | except Exception as e: 117 | logging.error(f"Error initializing server {self.name}: {e}") 118 | await self.cleanup() 119 | raise 120 | 121 | # 从服务器获取可用工具列表 122 | async def list_tools(self) -> List[Any]: 123 | # 如果 session 未初始化,抛出运行时异常 124 | if not self.session: 125 | raise RuntimeError(f"Server {self.name} not initialized") 126 | # 调用会话的 list_tools 方法获取工具响应 127 | tools_response = await self.session.list_tools() 128 | # 初始化空列表 tools 129 | tools = [] 130 | 131 | # 遍历工具响应,解析并存储工具信息 132 | for item in tools_response: 133 | if isinstance(item, tuple) and item[0] == 'tools': 134 | for tool in item[1]: 135 | tools.append(Tool(tool.name, tool.description, tool.inputSchema)) 136 | 137 | return tools 138 | 139 | # 执行指定工具,支持重试机制 140 | # tool_name: 工具名称 141 | # arguments: 执行工具所需的参数 142 | # retries: 最大重试次数 143 | # delay: 每次重试的间隔时间 144 | async def execute_tool(self, tool_name: str, arguments: Dict[str, Any], retries: int = 2, delay: float = 1.0) -> Any: 145 | # 检查会话是否已初始化 146 | if not self.session: 147 | raise RuntimeError(f"Server {self.name} not initialized") 148 | 149 | # 循环尝试执行工具 150 | # 如果成功,返回执行结果 151 | attempt = 0 152 | while attempt < retries: 153 | try: 154 | logging.info(f"Executing {tool_name}...") 155 | result = await self.session.call_tool(tool_name, arguments) 156 | return result 157 | 158 | # 捕获异常时记录日志 159 | # 如果未超出最大重试次数,延迟后重试 160 | # 达到最大重试次数时抛出异常 161 | except Exception as e: 162 | attempt += 1 163 | logging.warning(f"Error executing tool: {e}. Attempt {attempt} of {retries}.") 164 | if attempt < retries: 165 | logging.info(f"Retrying in {delay} seconds...") 166 | await asyncio.sleep(delay) 167 | else: 168 | logging.error("Max retries reached. Failing.") 169 | raise 170 | 171 | # 从服务器获取可用资源列表 172 | async def list_resources(self) -> List[Any]: 173 | # 如果 session 未初始化,抛出运行时异常 174 | if not self.session: 175 | raise RuntimeError(f"Server {self.name} not initialized") 176 | # 调用会话的 list_resources 方法获取资源响应 177 | resources_response = await self.session.list_resources() 178 | # 初始化空列表 resources 179 | resources = [] 180 | 181 | # 遍历资源响应,解析并存储资源信息 182 | for item in resources_response: 183 | if isinstance(item, tuple) and item[0] == 'resources': 184 | for resource in item[1]: 185 | resources.append(Resource(str(resource.uri), resource.name, resource.description, resource.mimeType)) 186 | 187 | return resources 188 | 189 | # 读取指定资源,支持重试机制 190 | # resource_uri: 资源URI标识 191 | # retries: 最大重试次数 192 | # delay: 每次重试的间隔时间 193 | async def read_resource(self, resource_uri: str, retries: int = 2, delay: float = 1.0) -> Any: 194 | # 检查会话是否已初始化 195 | if not self.session: 196 | raise RuntimeError(f"Server {self.name} not initialized") 197 | 198 | # 循环尝试执行工具 199 | # 如果成功,返回执行结果 200 | attempt = 0 201 | while attempt < retries: 202 | try: 203 | logging.info(f"Executing {resource_uri}...") 204 | result = await self.session.read_resource(resource_uri) 205 | 206 | return result 207 | 208 | # 捕获异常时记录日志 209 | # 如果未超出最大重试次数,延迟后重试 210 | # 达到最大重试次数时抛出异常 211 | except Exception as e: 212 | attempt += 1 213 | logging.warning(f"Error executing resource: {e}. Attempt {attempt} of {retries}.") 214 | if attempt < retries: 215 | logging.info(f"Retrying in {delay} seconds...") 216 | await asyncio.sleep(delay) 217 | else: 218 | logging.error("Max retries reached. Failing.") 219 | raise 220 | 221 | # 清理服务器资源,确保资源释放 222 | async def cleanup(self) -> None: 223 | # 使用异步锁确保清理操作的线程安全 224 | # 清理会话资源,记录可能的警告 225 | async with self._cleanup_lock: 226 | try: 227 | if self.session: 228 | try: 229 | await self.session.__aexit__(None, None, None) 230 | except Exception as e: 231 | logging.warning(f"Warning during session cleanup for {self.name}: {e}") 232 | finally: 233 | self.session = None 234 | 235 | # 清理标准输入/输出上下文资源,捕获并记录不同类型的异常 236 | if self.stdio_context: 237 | try: 238 | await self.stdio_context.__aexit__(None, None, None) 239 | except (RuntimeError, asyncio.CancelledError) as e: 240 | logging.info(f"Note: Normal shutdown message for {self.name}: {e}") 241 | except Exception as e: 242 | logging.warning(f"Warning during stdio cleanup for {self.name}: {e}") 243 | finally: 244 | self.stdio_context = None 245 | # 捕获清理过程中可能的异常并记录错误日志 246 | except Exception as e: 247 | logging.error(f"Error during cleanup of server {self.name}: {e}") 248 | 249 | 250 | # 代表各个资源及其属性和格式 251 | class Resource: 252 | # 构造函数,在类实例化时调用 253 | # uri: 表的唯一资源标识符 254 | # name: 资源的名称 255 | # mimeType: MIME 类型,表示资源的数据类型 256 | # description: 描述信息 257 | def __init__(self, uri: str, name: str, description: str, mimeType: str) -> None: 258 | self.uri: str = uri 259 | self.name: str = name 260 | self.description: str = description 261 | self.mimeType: str = mimeType 262 | 263 | # 将资源的信息格式化为一个字符串,适合语言模型(LLM)使用 264 | def format_for_llm(self) -> str: 265 | return f""" 266 | URI: {self.uri} 267 | Name: {self.name} 268 | Description: {self.description} 269 | MimeType: {self.mimeType} 270 | """ 271 | 272 | 273 | # 代表各个工具及其属性和格式 274 | class Tool: 275 | # 构造函数,在类实例化时调用 276 | # name: 工具的名称 277 | # description: 工具的描述信息 278 | # input_schema: 工具的输入架构,通常是一个描述输入参数的字典 279 | def __init__(self, name: str, description: str, input_schema: Dict[str, Any]) -> None: 280 | self.name: str = name 281 | self.description: str = description 282 | self.input_schema: Dict[str, Any] = input_schema 283 | 284 | # 将工具的信息格式化为一个字符串,适合语言模型(LLM)使用 285 | # 返回值: 包含工具名称、描述和参数信息的格式化字符串 286 | def format_for_llm(self) -> str: 287 | args_desc = [] 288 | if 'properties' in self.input_schema: 289 | for param_name, param_info in self.input_schema['properties'].items(): 290 | arg_desc = f"- {param_name}: {param_info.get('description', 'No description')}" 291 | if param_name in self.input_schema.get('required', []): 292 | arg_desc += " (required)" 293 | args_desc.append(arg_desc) 294 | 295 | return f""" 296 | Tool: {self.name} 297 | Description: {self.description} 298 | Arguments: 299 | {chr(10).join(args_desc)} 300 | """ 301 | 302 | 303 | # 管理与LLM的通信 304 | class LLMClient: 305 | def __init__(self, base_url: str, api_key: str, chat_model: str) -> None: 306 | self.base_url: str = base_url 307 | self.api_key: str = api_key 308 | self.chat_model: str = chat_model 309 | 310 | # 向 LLM 发送请求,并返回其响应 311 | # messages: 一个字典列表,每个字典包含消息内容,通常是聊天对话的一部分 312 | # 返回值: 返回 LLM 的响应内容,类型为字符串 313 | # 如果请求失败,抛出 RequestException 314 | def get_response(self, messages: List[Dict[str, str]]) -> str: 315 | # 指定 LLM 提供者的 API 端点,用于发送聊天请求 316 | url = self.base_url 317 | 318 | # Content-Type: 表示请求体的格式为 JSON 319 | # Authorization: 使用 Bearer 令牌进行身份验证,令牌为 self.api_key 320 | headers = { 321 | "Content-Type": "application/json", 322 | "Authorization": f"Bearer {self.api_key}" 323 | } 324 | 325 | # 请求体数据,以字典形式表示 326 | # "messages": 包含传入的消息列表 327 | # "model": 使用的模型名称(例如 llama-3.2-90b-vision-preview) 328 | # "temperature": 控制生成的文本的随机性,0.7 表示适度随机 329 | # "max_tokens": 最大的输出 token 数量(4096),限制响应的长度 330 | # "top_p": 控制响应中最可能的 token 的累积概率,1 表示使用所有候选词 331 | payload = { 332 | "messages": messages, 333 | "temperature": 1.0, 334 | "top_p": 1.0, 335 | "max_tokens": 4000, 336 | "model": self.chat_model 337 | } 338 | 339 | try: 340 | # 使用 requests.post 发送 POST 请求,传递 URL、请求头和负载 341 | # url: 请求的 API 端点 342 | # headers: 请求头,包含 Content-Type 和 Authorizatio 343 | # json=payload: 将 payload 作为 JSON 格式传递 344 | response = requests.post(url, headers=headers, json=payload) 345 | # 如果响应的状态码表示错误(例如 4xx 或 5xx),则抛出异常 346 | response.raise_for_status() 347 | # 解析响应为 JSON 格式的数据 348 | data = response.json() 349 | # 从 JSON 响应中提取工具的输出内容 350 | return data['choices'][0]['message']['content'] 351 | 352 | # 如果请求失败(如连接错误、超时或无效响应等),捕获 RequestException 异常 353 | # 记录错误信息,str(e) 提供异常的具体描述 354 | except requests.exceptions.RequestException as e: 355 | error_message = f"Error getting LLM response: {str(e)}" 356 | logging.error(error_message) 357 | # 如果异常中包含响应对象(e.response),进一步记录响应的状态码和响应内容 358 | # 这有助于分析请求失败的原因(例如服务端错误、API 限制等) 359 | if e.response is not None: 360 | status_code = e.response.status_code 361 | logging.error(f"Status code: {status_code}") 362 | logging.error(f"Response details: {e.response.text}") 363 | # 返回一个友好的错误消息给调用者,告知发生了错误并建议重试或重新措辞请求 364 | return f"I encountered an error: {error_message}. Please try again or rephrase your request." 365 | 366 | 367 | # 协调用户、 LLM和工具之间的交互 368 | class ChatSession: 369 | # servers: 一个 Server 类的列表,表示多个服务器的实例 370 | # llm_client: LLMClient 的实例,用于与 LLM 进行通信 371 | def __init__(self, servers: List[Server], llm_client: LLMClient) -> None: 372 | self.servers: List[Server] = servers 373 | self.llm_client: LLMClient = llm_client 374 | 375 | # 清理所有服务器 376 | # 创建清理任务: 遍历所有的服务器实例,为每个服务器创建一个清理任务(调用 server.cleanup()) 377 | # 并行执行清理任务: 使用 asyncio.gather 并发执行所有清理任务 378 | # return_exceptions=True 表示即使部分任务抛出异常,也会继续执行其它任务 379 | # 异常处理: 如果有异常,记录警告日志 380 | async def cleanup_servers(self) -> None: 381 | cleanup_tasks = [] 382 | for server in self.servers: 383 | cleanup_tasks.append(asyncio.create_task(server.cleanup())) 384 | 385 | if cleanup_tasks: 386 | try: 387 | await asyncio.gather(*cleanup_tasks, return_exceptions=True) 388 | except Exception as e: 389 | logging.warning(f"Warning during final cleanup: {e}") 390 | 391 | # 负责处理从 LLM 返回的响应,并在需要时执行工具 392 | async def process_llm_response(self, llm_response: str) -> str: 393 | try: 394 | # 尝试将 LLM 响应解析为 JSON ,以便检查是否包含 tool 和 arguments 字段 395 | llm_call = json.loads(llm_response) 396 | 397 | # 1、如果响应包含工具名称和参数,执行相应工具 398 | if "tool" in llm_call and "arguments" in llm_call: 399 | logging.info(f"Executing tool: {llm_call['tool']}") 400 | logging.info(f"With arguments: {llm_call['arguments']}") 401 | # 遍历每个服务器,检查是否有与响应中的工具名称匹配的工具 402 | for server in self.servers: 403 | tools = await server.list_tools() 404 | # 如果找到相应的工具 405 | if any(tool.name == llm_call["tool"] for tool in tools): 406 | try: 407 | # 调用 server.execute_tool 来执行该工具 408 | result = await server.execute_tool(llm_call["tool"], llm_call["arguments"]) 409 | # 如果工具执行返回一个包含进度的字典(progress 和 total),则记录进度信息 410 | if isinstance(result, dict) and 'progress' in result: 411 | progress = result['progress'] 412 | total = result['total'] 413 | logging.info(f"Progress: {progress}/{total} ({(progress/total)*100:.1f}%)") 414 | # 返回工具的执行结果 415 | return f"Tool execution result: {result}" 416 | # 如果无法找到指定的工具或遇到错误,返回相应的错误信息 417 | except Exception as e: 418 | error_msg = f"Error executing tool: {str(e)}" 419 | logging.error(error_msg) 420 | return error_msg 421 | return f"No server found with tool: {llm_call['tool']}" 422 | 423 | # 2、如果响应包含资源名称和URI,执行读取相应的资源 424 | if "resource" in llm_call : 425 | logging.info(f"Executing resource: {llm_call['resource']}") 426 | logging.info(f"With URI: {llm_call['uri']}") 427 | # 遍历每个服务器,检查是否有与响应中的资源名称匹配的资源 428 | for server in self.servers: 429 | resources = await server.list_resources() 430 | # 如果找到相应的资源 431 | if any(resource.uri == llm_call["uri"] for resource in resources): 432 | try: 433 | # 调用 server.read_resource 来读取该资源 434 | result = await server.read_resource(llm_call["uri"]) 435 | # 如果资源执行返回一个包含进度的字典(progress 和 total),则记录进度信息 436 | if isinstance(result, dict) and 'progress' in result: 437 | progress = result['progress'] 438 | total = result['total'] 439 | logging.info(f"Progress: {progress}/{total} ({(progress/total)*100:.1f}%)") 440 | # 返回资源的执行结果 441 | return f"Resource execution result: {result}" 442 | # 如果无法找到指定的资源或遇到错误,返回相应的错误信息 443 | except Exception as e: 444 | error_msg = f"Error executing resource: {str(e)}" 445 | logging.error(error_msg) 446 | return error_msg 447 | return f"No server found with resource: {llm_call['uri']}" 448 | 449 | return llm_response 450 | 451 | # 如果响应无法解析为 JSON,返回原始 LLM 响应 452 | except json.JSONDecodeError: 453 | return llm_response 454 | 455 | # 方法: start 用于启动整个聊天会话,初始化服务器并开始与用户的互动 456 | async def start(self) -> None: 457 | try: 458 | # 初始化所有服务器: 遍历 self.servers,异步调用每个服务器的 initialize 方法 459 | for server in self.servers: 460 | try: 461 | await server.initialize() 462 | # 异常处理: 如果服务器初始化失败,记录错误并调用 cleanup_servers 清理资源,然后退出会话 463 | except Exception as e: 464 | logging.error(f"Failed to initialize server: {e}") 465 | await self.cleanup_servers() 466 | return 467 | 468 | # 遍历所有服务器,调用 list_tools() 获取每个服务器的工具列表 469 | all_tools = [] 470 | for server in self.servers: 471 | tools = await server.list_tools() 472 | all_tools.extend(tools) 473 | # 将所有工具的描述信息汇总,生成供 LLM 使用的工具描述字符串 474 | tools_description = "\n".join([tool.format_for_llm() for tool in all_tools]) 475 | 476 | # 遍历所有服务器,调用 list_resources() 获取每个服务器的资源列表 477 | all_resources = [] 478 | for server in self.servers: 479 | resources = await server.list_resources() 480 | all_resources.extend(resources) 481 | # 将所有资源的描述信息汇总,生成供 LLM 使用的资源描述字符串 482 | resources_description = "\n".join([resource.format_for_llm() for resource in all_resources]) 483 | 484 | # 构建一个系统消息,作为 LLM 交互的指令,告知 LLM 使用哪些工具以及如何与用户进行交互 485 | # 系统消息强调 LLM 必须以严格的 JSON 格式请求工具,并且在工具响应后将其转换为自然语言响应 486 | system_message = f"""You are a helpful assistant with access to these resources and tools: 487 | 488 | resources:{resources_description} 489 | tools:{tools_description} 490 | Choose the appropriate resource or tool based on the user's question. If no resource or tool is needed, reply directly. 491 | 492 | IMPORTANT: When you need to use a resource, you must ONLY respond with the exact JSON object format below, nothing else: 493 | {{ 494 | "resource": "resource-name", 495 | "uri": "resource-URI" 496 | }} 497 | 498 | After receiving a resource's response: 499 | 1. Transform the raw data into a natural, conversational response 500 | 2. Keep responses concise but informative 501 | 3. Focus on the most relevant information 502 | 4. Use appropriate context from the user's question 503 | 5. Avoid simply repeating the raw data 504 | 505 | IMPORTANT: When you need to use a tool, you must ONLY respond with the exact JSON object format below, nothing else: 506 | {{ 507 | "tool": "tool-name", 508 | "arguments": {{ 509 | "argument-name": "value" 510 | }} 511 | }} 512 | 513 | After receiving a tool's response: 514 | 1. Transform the raw data into a natural, conversational response 515 | 2. Keep responses concise but informative 516 | 3. Focus on the most relevant information 517 | 4. Use appropriate context from the user's question 518 | 5. Avoid simply repeating the raw data 519 | 520 | Please use only the resources or tools that are explicitly defined above.""" 521 | 522 | # 消息初始化 创建一个消息列表,其中包含一个系统消息,指示 LLM 如何与用户交互 523 | messages = [ 524 | { 525 | "role": "system", 526 | "content": system_message 527 | } 528 | ] 529 | 530 | # 交互循环 531 | while True: 532 | try: 533 | # 等待用户输入,如果用户输入 quit 或 exit,则退出 534 | user_input = input("user: ").strip().lower() 535 | if user_input in ['quit', 'exit']: 536 | logging.info("\nExiting...") 537 | break 538 | 539 | # 将用户输入添加到消息列表 540 | messages.append({"role": "user", "content": user_input}) 541 | 542 | # 调用 LLM 客户端获取 LLM 的响应 543 | llm_response = self.llm_client.get_response(messages) 544 | logging.info("\nAssistant: %s", llm_response) 545 | 546 | # 调用 process_llm_response 方法处理 LLM 响应,执行工具(如果需要) 547 | result = await self.process_llm_response(llm_response) 548 | 549 | # 如果工具执行结果与 LLM 响应不同,更新消息列表并再次与 LLM 交互,获取最终响应 550 | if result != llm_response: 551 | messages.append({"role": "assistant", "content": llm_response}) 552 | messages.append({"role": "system", "content": result}) 553 | 554 | final_response = self.llm_client.get_response(messages) 555 | logging.info("\nFinal response: %s", final_response) 556 | messages.append({"role": "assistant", "content": final_response}) 557 | else: 558 | messages.append({"role": "assistant", "content": llm_response}) 559 | 560 | # 处理 KeyboardInterrupt,允许用户中断会话 561 | except KeyboardInterrupt: 562 | logging.info("\nExiting...") 563 | break 564 | 565 | # 清理资源: 无论会话如何结束(正常结束或由于异常退出),都确保调用 cleanup_servers 清理服务器资源 566 | finally: 567 | await self.cleanup_servers() 568 | 569 | 570 | async def main() -> None: 571 | # 创建一个 Configuration 类的实例 572 | config = Configuration() 573 | # 调用 Configuration 类中的 load_config 方法来加载配置文件 servers_config.json 574 | server_config = config.load_config('servers_config.json') 575 | # 遍历 server_config['mcpServers'] 字典,并为每个服务器创建一个 Server 实例,传入服务器名称 (name) 和配置信息 (srv_config) 576 | # 结果是一个 Server 对象的列表,保存在 servers 变量中 577 | servers = [Server(name, srv_config) for name, srv_config in server_config['mcpServers'].items()] 578 | # 创建一个 LLMClient 实例,用于与 LLM (大语言模型) 进行交互 579 | llm_client = LLMClient(config.llm_base_url, config.llm_api_key, config.llm_chat_model) 580 | # 创建一个 ChatSession 实例,负责管理与用户的聊天交互、LLM 响应和工具执行 581 | # 将之前创建的 servers 和 llm_client 传递给 ChatSession 构造函数,初始化会话 582 | chat_session = ChatSession(servers, llm_client) 583 | # 调用 ChatSession 类的 start 方法,启动聊天会话 584 | # 由于 start 是一个异步方法,所以使用 await 等待该方法执行完毕 585 | # start 方法将处理用户的输入、与 LLM 交互、执行工具(如果需要)等,并持续运行直到用户选择退出 586 | await chat_session.start() 587 | 588 | 589 | 590 | 591 | if __name__ == "__main__": 592 | asyncio.run(main()) 593 | -------------------------------------------------------------------------------- /nangeAGICode/search_mysql_filesystem_chat/client_chat.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import logging 4 | import os 5 | import shutil 6 | from typing import Dict, List, Optional, Any 7 | import requests 8 | from dotenv import load_dotenv 9 | from mcp import ClientSession, StdioServerParameters 10 | from mcp.client.stdio import stdio_client 11 | 12 | # 配置日志记录 13 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') 14 | 15 | 16 | # 用于管理MCP客户端的配置和环境变量 17 | class Configuration: 18 | # 初始化对象,并加载环境变量 19 | def __init__(self) -> None: 20 | # 加载环境变量(通常从 .env 文件中读取) 21 | self.load_env() 22 | self.base_url = os.getenv("LLM_BASE_URL") 23 | self.api_key = os.getenv("LLM_API_KEY") 24 | self.chat_model = os.getenv("LLM_CHAT_MODEL") 25 | 26 | # @staticmethod,表示该方法不依赖于实例本身,可以直接通过类名调用 27 | @staticmethod 28 | def load_env() -> None: 29 | load_dotenv() 30 | 31 | # 从指定的 JSON 配置文件中加载配置 32 | # file_path: 配置文件的路径 33 | # 返回值: 一个包含配置信息的字典 34 | # FileNotFoundError: 文件不存在时抛出 35 | # JSONDecodeError: 配置文件不是有效的 JSON 格式时抛出 36 | @staticmethod 37 | def load_config(file_path: str) -> Dict[str, Any]: 38 | # 打开指定路径的文件,以只读模式读取 39 | # 使用 json.load 将文件内容解析为 Python 字典并返回 40 | with open(file_path, 'r') as f: 41 | return json.load(f) 42 | 43 | # @property,将方法转换为只读属性,调用时不需要括号 44 | # 提供获取 llm_api_key 的接口 45 | @property 46 | def llm_api_key(self) -> str: 47 | # 检查 self.api_key 是否存在 48 | if not self.api_key: 49 | # 如果不存在,抛出 ValueError 异常 50 | raise ValueError("LLM_API_KEY not found in environment variables") 51 | # 返回 self.api_key 的值 52 | return self.api_key 53 | 54 | # @property,将方法转换为只读属性,调用时不需要括号 55 | # 提供获取 llm_base_url 的接口 56 | @property 57 | def llm_base_url(self) -> str: 58 | # 检查 self.base_url 是否存在 59 | if not self.base_url: 60 | # 如果不存在,抛出 ValueError 异常 61 | raise ValueError("LLM_BASE_URL not found in environment variables") 62 | # 返回 self.base_url 的值 63 | return self.base_url 64 | 65 | # @property,将方法转换为只读属性,调用时不需要括号 66 | # 提供获取 llm_chat_model 的接口 67 | @property 68 | def llm_chat_model(self) -> str: 69 | # 检查 self.base_url 是否存在 70 | if not self.chat_model: 71 | # 如果不存在,抛出 ValueError 异常 72 | raise ValueError("LLM_CHAT_MODEL not found in environment variables") 73 | # 返回 self.base_url 的值 74 | return self.chat_model 75 | 76 | 77 | # 处理 MCP 服务器初始化、工具发现和执行 78 | class Server: 79 | # 构造函数,在类实例化时调用 80 | # name: 服务器的名称 81 | # config: 配置字典,包含服务器的参数 82 | def __init__(self, name: str, config: Dict[str, Any]) -> None: 83 | self.name: str = name 84 | self.config: Dict[str, Any] = config 85 | # 标准输入/输出的上下文对象,用于与服务器交互 86 | self.stdio_context: Optional[Any] = None 87 | # 服务器的会话,用于发送请求和接收响应 88 | self.session: Optional[ClientSession] = None 89 | # 异步锁,用于确保清理资源的过程是线程安全的 90 | self._cleanup_lock: asyncio.Lock = asyncio.Lock() 91 | # 存储服务器的能力 92 | self.capabilities: Optional[Dict[str, Any]] = None 93 | 94 | # 初始化服务器连接 95 | async def initialize(self) -> None: 96 | # server_params: 创建服务器参数对象 97 | # command: 如果配置中命令是 npx,使用系统路径查找,否则直接使用配置值 98 | # args: 从配置中获取命令行参数 99 | # env: 合并系统环境变量和配置中的自定义环境变量 100 | server_params = StdioServerParameters( 101 | command=shutil.which("npx") if self.config['command'] == "npx" else self.config['command'], 102 | args=self.config['args'], 103 | env={**os.environ, **self.config['env']} if self.config.get('env') else None 104 | ) 105 | try: 106 | # 使用 stdio_client 初始化标准输入/输出上下文 107 | self.stdio_context = stdio_client(server_params) 108 | read, write = await self.stdio_context.__aenter__() 109 | # 创建 ClientSession 会话,并调用其 initialize 方法以获取服务器能力 110 | self.session = ClientSession(read, write) 111 | await self.session.__aenter__() 112 | self.capabilities = await self.session.initialize() 113 | # 发生异常时记录错误日志,调用 cleanup 清理资源并重新抛出异常 114 | except Exception as e: 115 | logging.error(f"Error initializing server {self.name}: {e}") 116 | await self.cleanup() 117 | raise 118 | 119 | # 从服务器获取可用工具列表 120 | async def list_tools(self) -> List[Any]: 121 | # 如果 session 未初始化,抛出运行时异常 122 | if not self.session: 123 | raise RuntimeError(f"Server {self.name} not initialized") 124 | # 调用会话的 list_tools 方法获取工具响应 125 | tools_response = await self.session.list_tools() 126 | # 初始化空列表 tools 127 | tools = [] 128 | 129 | # 遍历工具响应,解析并存储工具信息 130 | for item in tools_response: 131 | if isinstance(item, tuple) and item[0] == 'tools': 132 | for tool in item[1]: 133 | tools.append(Tool(tool.name, tool.description, tool.inputSchema)) 134 | 135 | return tools 136 | 137 | # 执行指定工具,支持重试机制 138 | # tool_name: 工具名称 139 | # arguments: 执行工具所需的参数 140 | # retries: 最大重试次数 141 | # delay: 每次重试的间隔时间 142 | async def execute_tool(self, tool_name: str, arguments: Dict[str, Any], retries: int = 2, 143 | delay: float = 1.0) -> Any: 144 | # 检查会话是否已初始化 145 | if not self.session: 146 | raise RuntimeError(f"Server {self.name} not initialized") 147 | 148 | # 循环尝试执行工具 149 | # 如果成功,返回执行结果 150 | attempt = 0 151 | while attempt < retries: 152 | try: 153 | logging.info(f"Executing {tool_name}...") 154 | result = await self.session.call_tool(tool_name, arguments) 155 | return result 156 | 157 | # 捕获异常时记录日志 158 | # 如果未超出最大重试次数,延迟后重试 159 | # 达到最大重试次数时抛出异常 160 | except Exception as e: 161 | attempt += 1 162 | logging.warning(f"Error executing tool: {e}. Attempt {attempt} of {retries}.") 163 | if attempt < retries: 164 | logging.info(f"Retrying in {delay} seconds...") 165 | await asyncio.sleep(delay) 166 | else: 167 | logging.error("Max retries reached. Failing.") 168 | raise 169 | 170 | # 从服务器获取可用资源列表 171 | async def list_resources(self) -> List[Any]: 172 | # 如果 session 未初始化,抛出运行时异常 173 | if not self.session: 174 | raise RuntimeError(f"Server {self.name} not initialized") 175 | # 调用会话的 list_resources 方法获取资源响应 176 | resources_response = await self.session.list_resources() 177 | # 初始化空列表 resources 178 | resources = [] 179 | 180 | # 遍历资源响应,解析并存储资源信息 181 | for item in resources_response: 182 | if isinstance(item, tuple) and item[0] == 'resources': 183 | for resource in item[1]: 184 | resources.append( 185 | Resource(str(resource.uri), resource.name, resource.description, resource.mimeType)) 186 | 187 | return resources 188 | 189 | # 读取指定资源,支持重试机制 190 | # resource_uri: 资源URI标识 191 | # retries: 最大重试次数 192 | # delay: 每次重试的间隔时间 193 | async def read_resource(self, resource_uri: str, retries: int = 2, delay: float = 1.0) -> Any: 194 | # 检查会话是否已初始化 195 | if not self.session: 196 | raise RuntimeError(f"Server {self.name} not initialized") 197 | 198 | # 循环尝试执行工具 199 | # 如果成功,返回执行结果 200 | attempt = 0 201 | while attempt < retries: 202 | try: 203 | logging.info(f"Executing {resource_uri}...") 204 | result = await self.session.read_resource(resource_uri) 205 | 206 | return result 207 | 208 | # 捕获异常时记录日志 209 | # 如果未超出最大重试次数,延迟后重试 210 | # 达到最大重试次数时抛出异常 211 | except Exception as e: 212 | attempt += 1 213 | logging.warning(f"Error executing resource: {e}. Attempt {attempt} of {retries}.") 214 | if attempt < retries: 215 | logging.info(f"Retrying in {delay} seconds...") 216 | await asyncio.sleep(delay) 217 | else: 218 | logging.error("Max retries reached. Failing.") 219 | raise 220 | 221 | # 清理服务器资源,确保资源释放 222 | async def cleanup(self) -> None: 223 | # 使用异步锁确保清理操作的线程安全 224 | # 清理会话资源,记录可能的警告 225 | async with self._cleanup_lock: 226 | try: 227 | if self.session: 228 | try: 229 | await self.session.__aexit__(None, None, None) 230 | except Exception as e: 231 | logging.warning(f"Warning during session cleanup for {self.name}: {e}") 232 | finally: 233 | self.session = None 234 | 235 | # 清理标准输入/输出上下文资源,捕获并记录不同类型的异常 236 | if self.stdio_context: 237 | try: 238 | await self.stdio_context.__aexit__(None, None, None) 239 | except (RuntimeError, asyncio.CancelledError) as e: 240 | logging.info(f"Note: Normal shutdown message for {self.name}: {e}") 241 | except Exception as e: 242 | logging.warning(f"Warning during stdio cleanup for {self.name}: {e}") 243 | finally: 244 | self.stdio_context = None 245 | # 捕获清理过程中可能的异常并记录错误日志 246 | except Exception as e: 247 | logging.error(f"Error during cleanup of server {self.name}: {e}") 248 | 249 | 250 | # 代表各个资源及其属性和格式 251 | class Resource: 252 | # 构造函数,在类实例化时调用 253 | # uri: 表的唯一资源标识符 254 | # name: 资源的名称 255 | # mimeType: MIME 类型,表示资源的数据类型 256 | # description: 描述信息 257 | def __init__(self, uri: str, name: str, description: str, mimeType: str) -> None: 258 | self.uri: str = uri 259 | self.name: str = name 260 | self.description: str = description 261 | self.mimeType: str = mimeType 262 | 263 | # 将资源的信息格式化为一个字符串,适合语言模型(LLM)使用 264 | def format_for_llm(self) -> str: 265 | return f""" 266 | URI: {self.uri} 267 | Name: {self.name} 268 | Description: {self.description} 269 | MimeType: {self.mimeType} 270 | """ 271 | 272 | 273 | # 代表各个工具及其属性和格式 274 | class Tool: 275 | # 构造函数,在类实例化时调用 276 | # name: 工具的名称 277 | # description: 工具的描述信息 278 | # input_schema: 工具的输入架构,通常是一个描述输入参数的字典 279 | def __init__(self, name: str, description: str, input_schema: Dict[str, Any]) -> None: 280 | self.name: str = name 281 | self.description: str = description 282 | self.input_schema: Dict[str, Any] = input_schema 283 | 284 | # 将工具的信息格式化为一个字符串,适合语言模型(LLM)使用 285 | # 返回值: 包含工具名称、描述和参数信息的格式化字符串 286 | def format_for_llm(self) -> str: 287 | args_desc = [] 288 | if 'properties' in self.input_schema: 289 | for param_name, param_info in self.input_schema['properties'].items(): 290 | arg_desc = f"- {param_name}: {param_info.get('description', 'No description')}" 291 | if param_name in self.input_schema.get('required', []): 292 | arg_desc += " (required)" 293 | args_desc.append(arg_desc) 294 | 295 | return f""" 296 | Tool: {self.name} 297 | Description: {self.description} 298 | Arguments: 299 | {chr(10).join(args_desc)} 300 | """ 301 | 302 | 303 | # 管理与LLM的通信 304 | class LLMClient: 305 | def __init__(self, base_url: str, api_key: str, chat_model: str) -> None: 306 | self.base_url: str = base_url 307 | self.api_key: str = api_key 308 | self.chat_model: str = chat_model 309 | 310 | # 向 LLM 发送请求,并返回其响应 311 | # messages: 一个字典列表,每个字典包含消息内容,通常是聊天对话的一部分 312 | # 返回值: 返回 LLM 的响应内容,类型为字符串 313 | # 如果请求失败,抛出 RequestException 314 | def get_response(self, messages: List[Dict[str, str]]) -> str: 315 | # 指定 LLM 提供者的 API 端点,用于发送聊天请求 316 | url = self.base_url 317 | 318 | # Content-Type: 表示请求体的格式为 JSON 319 | # Authorization: 使用 Bearer 令牌进行身份验证,令牌为 self.api_key 320 | headers = { 321 | "Content-Type": "application/json", 322 | "Authorization": f"Bearer {self.api_key}" 323 | } 324 | 325 | # 请求体数据,以字典形式表示 326 | # "messages": 包含传入的消息列表 327 | # "model": 使用的模型名称(例如 llama-3.2-90b-vision-preview) 328 | # "temperature": 控制生成的文本的随机性,0.7 表示适度随机 329 | # "max_tokens": 最大的输出 token 数量(4096),限制响应的长度 330 | # "top_p": 控制响应中最可能的 token 的累积概率,1 表示使用所有候选词 331 | payload = { 332 | "messages": messages, 333 | "temperature": 1.0, 334 | "top_p": 1.0, 335 | "max_tokens": 4000, 336 | "model": self.chat_model 337 | } 338 | 339 | try: 340 | # 使用 requests.post 发送 POST 请求,传递 URL、请求头和负载 341 | # url: 请求的 API 端点 342 | # headers: 请求头,包含 Content-Type 和 Authorizatio 343 | # json=payload: 将 payload 作为 JSON 格式传递 344 | response = requests.post(url, headers=headers, json=payload) 345 | # 如果响应的状态码表示错误(例如 4xx 或 5xx),则抛出异常 346 | response.raise_for_status() 347 | # 解析响应为 JSON 格式的数据 348 | data = response.json() 349 | # 从 JSON 响应中提取工具的输出内容 350 | return data['choices'][0]['message']['content'] 351 | 352 | # 如果请求失败(如连接错误、超时或无效响应等),捕获 RequestException 异常 353 | # 记录错误信息,str(e) 提供异常的具体描述 354 | except requests.exceptions.RequestException as e: 355 | error_message = f"Error getting LLM response: {str(e)}" 356 | logging.error(error_message) 357 | # 如果异常中包含响应对象(e.response),进一步记录响应的状态码和响应内容 358 | # 这有助于分析请求失败的原因(例如服务端错误、API 限制等) 359 | if e.response is not None: 360 | status_code = e.response.status_code 361 | logging.error(f"Status code: {status_code}") 362 | logging.error(f"Response details: {e.response.text}") 363 | # 返回一个友好的错误消息给调用者,告知发生了错误并建议重试或重新措辞请求 364 | return f"I encountered an error: {error_message}. Please try again or rephrase your request." 365 | 366 | 367 | # 协调用户、 LLM和工具之间的交互 368 | class ChatSession: 369 | # servers: 一个 Server 类的列表,表示多个服务器的实例 370 | # llm_client: LLMClient 的实例,用于与 LLM 进行通信 371 | def __init__(self, servers: List[Server], llm_client: LLMClient) -> None: 372 | self.servers: List[Server] = servers 373 | self.llm_client: LLMClient = llm_client 374 | 375 | # 清理所有服务器 376 | # 创建清理任务: 遍历所有的服务器实例,为每个服务器创建一个清理任务(调用 server.cleanup()) 377 | # 并行执行清理任务: 使用 asyncio.gather 并发执行所有清理任务 378 | # return_exceptions=True 表示即使部分任务抛出异常,也会继续执行其它任务 379 | # 异常处理: 如果有异常,记录警告日志 380 | async def cleanup_servers(self) -> None: 381 | cleanup_tasks = [] 382 | for server in self.servers: 383 | cleanup_tasks.append(asyncio.create_task(server.cleanup())) 384 | 385 | if cleanup_tasks: 386 | try: 387 | await asyncio.gather(*cleanup_tasks, return_exceptions=True) 388 | except Exception as e: 389 | logging.warning(f"Warning during final cleanup: {e}") 390 | 391 | # 负责处理从 LLM 返回的响应,并在需要时执行工具 392 | async def process_llm_response(self, llm_response: str) -> str: 393 | try: 394 | # 尝试将 LLM 响应解析为 JSON ,以便检查是否包含 tool 和 arguments 字段 395 | llm_call = json.loads(llm_response) 396 | 397 | # 1、如果响应包含工具名称和参数,执行相应工具 398 | if "tool" in llm_call and "arguments" in llm_call: 399 | logging.info(f"Executing tool: {llm_call['tool']}") 400 | logging.info(f"With arguments: {llm_call['arguments']}") 401 | # 遍历每个服务器,检查是否有与响应中的工具名称匹配的工具 402 | for server in self.servers: 403 | try: 404 | tools = await server.list_tools() 405 | # 如果找到相应的工具 406 | if any(tool.name == llm_call["tool"] for tool in tools): 407 | try: 408 | # 调用 server.execute_tool 来执行该工具 409 | result = await server.execute_tool(llm_call["tool"], llm_call["arguments"]) 410 | # 返回工具的执行结果 411 | return f"Tool execution result: {result}" 412 | # 如果无法找到指定的工具或遇到错误,返回相应的错误信息 413 | except Exception as e: 414 | error_msg = f"Error executing tool: {str(e)}" 415 | logging.error(error_msg) 416 | return error_msg 417 | except Exception as e: 418 | logging.error(f"Server {server.name} does not have 'list_tools' method: {e}") 419 | continue 420 | return f"No server found with tool: {llm_call['tool']}" 421 | 422 | # 2、如果响应包含资源名称和URI,执行读取相应的资源 423 | if "resource" in llm_call: 424 | logging.info(f"Executing resource: {llm_call['resource']}") 425 | logging.info(f"With URI: {llm_call['uri']}") 426 | # 遍历每个服务器,检查是否有与响应中的资源名称匹配的资源 427 | for server in self.servers: 428 | try: 429 | resources = await server.list_resources() 430 | # 如果找到相应的资源 431 | if any(resource.uri == llm_call["uri"] for resource in resources): 432 | try: 433 | # 调用 server.read_resource 来读取该资源 434 | result = await server.read_resource(llm_call["uri"]) 435 | # 返回资源的执行结果 436 | return f"Resource execution result: {result}" 437 | # 如果无法找到指定的资源或遇到错误,返回相应的错误信息 438 | except Exception as e: 439 | error_msg = f"Error executing resource: {str(e)}" 440 | logging.error(error_msg) 441 | return error_msg 442 | except Exception as e: 443 | logging.error(f"Server {server.name} does not have 'list_resources' method: {e}") 444 | continue 445 | return f"No server found with resource: {llm_call['uri']}" 446 | return llm_response 447 | 448 | # 如果响应无法解析为 JSON,返回原始 LLM 响应 449 | except json.JSONDecodeError: 450 | return llm_response 451 | 452 | # 方法: start 用于启动整个聊天会话,初始化服务器并开始与用户的互动 453 | async def start(self) -> None: 454 | try: 455 | # 初始化所有服务器: 遍历 self.servers,异步调用每个服务器的 initialize 方法 456 | for server in self.servers: 457 | try: 458 | await server.initialize() 459 | # 异常处理: 如果服务器初始化失败,记录错误并调用 cleanup_servers 清理资源,然后退出会话 460 | except Exception as e: 461 | logging.error(f"Failed to initialize server: {e}") 462 | await self.cleanup_servers() 463 | return 464 | 465 | # 遍历所有服务器,调用 list_tools() 获取每个服务器的工具列表 466 | all_tools = [] 467 | for server in self.servers: 468 | try: 469 | tools = await server.list_tools() 470 | all_tools.extend(tools) 471 | except Exception as e: 472 | print(f"Server {server.name} does not have 'list_tools' method: {e}") 473 | # 将所有工具的描述信息汇总,生成供 LLM 使用的工具描述字符串 474 | try: 475 | tools_description = "\n".join([tool.format_for_llm() for tool in all_tools]) 476 | except Exception as e: 477 | print(f"Error while formatting resources for LLM: {e}") 478 | tools_description = "" 479 | 480 | # # 遍历所有服务器,调用 list_resources() 获取每个服务器的资源列表 481 | all_resources = [] 482 | for server in self.servers: 483 | try: 484 | resources = await server.list_resources() 485 | all_resources.extend(resources) 486 | except Exception as e: 487 | print(f"Server {server.name} does not have 'list_resources' method: {e}") 488 | # 将所有资源的描述信息汇总,生成供 LLM 使用的资源描述字符串 489 | try: 490 | resources_description = "\n".join([resource.format_for_llm() for resource in all_resources]) 491 | except Exception as e: 492 | print(f"Error while formatting resources for LLM: {e}") 493 | resources_description = "" 494 | 495 | # 构建一个系统消息,作为 LLM 交互的指令,告知 LLM 使用哪些工具以及如何与用户进行交互 496 | # 系统消息强调 LLM 必须以严格的 JSON 格式请求工具,并且在工具响应后将其转换为自然语言响应 497 | system_message = f"""You are a helpful assistant with access to these resources and tools: 498 | 499 | resources:{resources_description} 500 | tools:{tools_description} 501 | Choose the appropriate resource or tool based on the user's question. If no resource or tool is needed, reply directly. 502 | 503 | IMPORTANT: When you need to use a resource, you must ONLY respond with the exact JSON object format below, nothing else: 504 | {{ 505 | "resource": "resource-name", 506 | "uri": "resource-URI" 507 | }} 508 | 509 | After receiving a resource's response: 510 | 1. Transform the raw data into a natural, conversational response 511 | 2. Keep responses concise but informative 512 | 3. Focus on the most relevant information 513 | 4. Use appropriate context from the user's question 514 | 5. Avoid simply repeating the raw data 515 | 516 | IMPORTANT: When you need to use a tool, you must ONLY respond with the exact JSON object format below, nothing else: 517 | {{ 518 | "tool": "tool-name", 519 | "arguments": {{ 520 | "argument-name": "value" 521 | }} 522 | }} 523 | 524 | After receiving a tool's response: 525 | 1. Transform the raw data into a natural, conversational response 526 | 2. Keep responses concise but informative 527 | 3. Focus on the most relevant information 528 | 4. Use appropriate context from the user's question 529 | 5. Avoid simply repeating the raw data 530 | 531 | Please use only the resources or tools that are explicitly defined above.""" 532 | 533 | # 消息初始化 创建一个消息列表,其中包含一个系统消息,指示 LLM 如何与用户交互 534 | messages = [ 535 | { 536 | "role": "system", 537 | "content": system_message 538 | } 539 | ] 540 | 541 | # 交互循环 542 | while True: 543 | try: 544 | # 等待用户输入,如果用户输入 quit 或 exit,则退出 545 | user_input = input("user: ").strip().lower() 546 | if user_input in ['quit', 'exit']: 547 | logging.info("\nExiting...") 548 | break 549 | 550 | # 将用户输入添加到消息列表 551 | messages.append({"role": "user", "content": user_input}) 552 | 553 | # 调用 LLM 客户端获取 LLM 的响应 554 | llm_response = self.llm_client.get_response(messages) 555 | logging.info("\nAssistant: %s", llm_response) 556 | 557 | # 调用 process_llm_response 方法处理 LLM 响应,执行工具(如果需要) 558 | result = await self.process_llm_response(llm_response) 559 | 560 | # 如果工具执行结果与 LLM 响应不同,更新消息列表并再次与 LLM 交互,获取最终响应 561 | if result != llm_response: 562 | messages.append({"role": "assistant", "content": llm_response}) 563 | messages.append({"role": "system", "content": result}) 564 | 565 | final_response = self.llm_client.get_response(messages) 566 | logging.info("\nFinal response: %s", final_response) 567 | messages.append({"role": "assistant", "content": final_response}) 568 | else: 569 | messages.append({"role": "assistant", "content": llm_response}) 570 | 571 | # 处理 KeyboardInterrupt,允许用户中断会话 572 | except KeyboardInterrupt: 573 | logging.info("\nExiting...") 574 | break 575 | # 清理资源: 无论会话如何结束(正常结束或由于异常退出),都确保调用 cleanup_servers 清理服务器资源 576 | finally: 577 | await self.cleanup_servers() 578 | 579 | 580 | async def main() -> None: 581 | # 创建一个 Configuration 类的实例 582 | config = Configuration() 583 | # 调用 Configuration 类中的 load_config 方法来加载配置文件 servers_config.json 584 | server_config = config.load_config('servers_config.json') 585 | # 遍历 server_config['mcpServers'] 字典,并为每个服务器创建一个 Server 实例,传入服务器名称 (name) 和配置信息 (srv_config) 586 | # 结果是一个 Server 对象的列表,保存在 servers 变量中 587 | servers = [Server(name, srv_config) for name, srv_config in server_config['mcpServers'].items()] 588 | # 创建一个 LLMClient 实例,用于与 LLM (大语言模型) 进行交互 589 | llm_client = LLMClient(config.llm_base_url, config.llm_api_key, config.llm_chat_model) 590 | # 创建一个 ChatSession 实例,负责管理与用户的聊天交互、LLM 响应和工具执行 591 | # 将之前创建的 servers 和 llm_client 传递给 ChatSession 构造函数,初始化会话 592 | chat_session = ChatSession(servers, llm_client) 593 | # 调用 ChatSession 类的 start 方法,启动聊天会话 594 | # 由于 start 是一个异步方法,所以使用 await 等待该方法执行完毕 595 | # start 方法将处理用户的输入、与 LLM 交互、执行工具(如果需要)等,并持续运行直到用户选择退出 596 | await chat_session.start() 597 | 598 | 599 | if __name__ == "__main__": 600 | asyncio.run(main()) 601 | 602 | -------------------------------------------------------------------------------- /nangeAGICode/mysql_filesystem_chat/client_chat.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import logging 4 | import os 5 | import shutil 6 | from typing import Dict, List, Optional, Any 7 | import requests 8 | from dotenv import load_dotenv 9 | from mcp import ClientSession, StdioServerParameters 10 | from mcp.client.stdio import stdio_client 11 | 12 | 13 | 14 | 15 | # 配置日志记录 16 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') 17 | 18 | # 用于管理MCP客户端的配置和环境变量 19 | class Configuration: 20 | # 初始化对象,并加载环境变量 21 | def __init__(self) -> None: 22 | # 加载环境变量(通常从 .env 文件中读取) 23 | self.load_env() 24 | self.base_url = os.getenv("LLM_BASE_URL") 25 | self.api_key = os.getenv("LLM_API_KEY") 26 | self.chat_model = os.getenv("LLM_CHAT_MODEL") 27 | 28 | # @staticmethod,表示该方法不依赖于实例本身,可以直接通过类名调用 29 | @staticmethod 30 | def load_env() -> None: 31 | load_dotenv() 32 | 33 | # 从指定的 JSON 配置文件中加载配置 34 | # file_path: 配置文件的路径 35 | # 返回值: 一个包含配置信息的字典 36 | # FileNotFoundError: 文件不存在时抛出 37 | # JSONDecodeError: 配置文件不是有效的 JSON 格式时抛出 38 | @staticmethod 39 | def load_config(file_path: str) -> Dict[str, Any]: 40 | # 打开指定路径的文件,以只读模式读取 41 | # 使用 json.load 将文件内容解析为 Python 字典并返回 42 | with open(file_path, 'r') as f: 43 | return json.load(f) 44 | 45 | # @property,将方法转换为只读属性,调用时不需要括号 46 | # 提供获取 llm_api_key 的接口 47 | @property 48 | def llm_api_key(self) -> str: 49 | # 检查 self.api_key 是否存在 50 | if not self.api_key: 51 | # 如果不存在,抛出 ValueError 异常 52 | raise ValueError("LLM_API_KEY not found in environment variables") 53 | # 返回 self.api_key 的值 54 | return self.api_key 55 | 56 | # @property,将方法转换为只读属性,调用时不需要括号 57 | # 提供获取 llm_base_url 的接口 58 | @property 59 | def llm_base_url(self) -> str: 60 | # 检查 self.base_url 是否存在 61 | if not self.base_url: 62 | # 如果不存在,抛出 ValueError 异常 63 | raise ValueError("LLM_BASE_URL not found in environment variables") 64 | # 返回 self.base_url 的值 65 | return self.base_url 66 | 67 | # @property,将方法转换为只读属性,调用时不需要括号 68 | # 提供获取 llm_chat_model 的接口 69 | @property 70 | def llm_chat_model(self) -> str: 71 | # 检查 self.base_url 是否存在 72 | if not self.chat_model: 73 | # 如果不存在,抛出 ValueError 异常 74 | raise ValueError("LLM_CHAT_MODEL not found in environment variables") 75 | # 返回 self.base_url 的值 76 | return self.chat_model 77 | 78 | 79 | # 处理 MCP 服务器初始化、工具发现和执行 80 | class Server: 81 | # 构造函数,在类实例化时调用 82 | # name: 服务器的名称 83 | # config: 配置字典,包含服务器的参数 84 | def __init__(self, name: str, config: Dict[str, Any]) -> None: 85 | self.name: str = name 86 | self.config: Dict[str, Any] = config 87 | # 标准输入/输出的上下文对象,用于与服务器交互 88 | self.stdio_context: Optional[Any] = None 89 | # 服务器的会话,用于发送请求和接收响应 90 | self.session: Optional[ClientSession] = None 91 | # 异步锁,用于确保清理资源的过程是线程安全的 92 | self._cleanup_lock: asyncio.Lock = asyncio.Lock() 93 | # 存储服务器的能力 94 | self.capabilities: Optional[Dict[str, Any]] = None 95 | 96 | # 初始化服务器连接 97 | async def initialize(self) -> None: 98 | # server_params: 创建服务器参数对象 99 | # command: 如果配置中命令是 npx,使用系统路径查找,否则直接使用配置值 100 | # args: 从配置中获取命令行参数 101 | # env: 合并系统环境变量和配置中的自定义环境变量 102 | server_params = StdioServerParameters( 103 | command=shutil.which("npx") if self.config['command'] == "npx" else self.config['command'], 104 | args=self.config['args'], 105 | env={**os.environ, **self.config['env']} if self.config.get('env') else None 106 | ) 107 | try: 108 | # 使用 stdio_client 初始化标准输入/输出上下文 109 | self.stdio_context = stdio_client(server_params) 110 | read, write = await self.stdio_context.__aenter__() 111 | # 创建 ClientSession 会话,并调用其 initialize 方法以获取服务器能力 112 | self.session = ClientSession(read, write) 113 | await self.session.__aenter__() 114 | self.capabilities = await self.session.initialize() 115 | # 发生异常时记录错误日志,调用 cleanup 清理资源并重新抛出异常 116 | except Exception as e: 117 | logging.error(f"Error initializing server {self.name}: {e}") 118 | await self.cleanup() 119 | raise 120 | 121 | # 从服务器获取可用工具列表 122 | async def list_tools(self) -> List[Any]: 123 | # 如果 session 未初始化,抛出运行时异常 124 | if not self.session: 125 | raise RuntimeError(f"Server {self.name} not initialized") 126 | # 调用会话的 list_tools 方法获取工具响应 127 | tools_response = await self.session.list_tools() 128 | # 初始化空列表 tools 129 | tools = [] 130 | 131 | # 遍历工具响应,解析并存储工具信息 132 | for item in tools_response: 133 | if isinstance(item, tuple) and item[0] == 'tools': 134 | for tool in item[1]: 135 | tools.append(Tool(tool.name, tool.description, tool.inputSchema)) 136 | 137 | return tools 138 | 139 | # 执行指定工具,支持重试机制 140 | # tool_name: 工具名称 141 | # arguments: 执行工具所需的参数 142 | # retries: 最大重试次数 143 | # delay: 每次重试的间隔时间 144 | async def execute_tool(self, tool_name: str, arguments: Dict[str, Any], retries: int = 2, delay: float = 1.0) -> Any: 145 | # 检查会话是否已初始化 146 | if not self.session: 147 | raise RuntimeError(f"Server {self.name} not initialized") 148 | 149 | # 循环尝试执行工具 150 | # 如果成功,返回执行结果 151 | attempt = 0 152 | while attempt < retries: 153 | try: 154 | logging.info(f"Executing {tool_name}...") 155 | result = await self.session.call_tool(tool_name, arguments) 156 | return result 157 | 158 | # 捕获异常时记录日志 159 | # 如果未超出最大重试次数,延迟后重试 160 | # 达到最大重试次数时抛出异常 161 | except Exception as e: 162 | attempt += 1 163 | logging.warning(f"Error executing tool: {e}. Attempt {attempt} of {retries}.") 164 | if attempt < retries: 165 | logging.info(f"Retrying in {delay} seconds...") 166 | await asyncio.sleep(delay) 167 | else: 168 | logging.error("Max retries reached. Failing.") 169 | raise 170 | 171 | # 从服务器获取可用资源列表 172 | async def list_resources(self) -> List[Any]: 173 | # 如果 session 未初始化,抛出运行时异常 174 | if not self.session: 175 | raise RuntimeError(f"Server {self.name} not initialized") 176 | # 调用会话的 list_resources 方法获取资源响应 177 | resources_response = await self.session.list_resources() 178 | # 初始化空列表 resources 179 | resources = [] 180 | 181 | # 遍历资源响应,解析并存储资源信息 182 | for item in resources_response: 183 | if isinstance(item, tuple) and item[0] == 'resources': 184 | for resource in item[1]: 185 | resources.append(Resource(str(resource.uri), resource.name, resource.description, resource.mimeType)) 186 | 187 | return resources 188 | 189 | # 读取指定资源,支持重试机制 190 | # resource_uri: 资源URI标识 191 | # retries: 最大重试次数 192 | # delay: 每次重试的间隔时间 193 | async def read_resource(self, resource_uri: str, retries: int = 2, delay: float = 1.0) -> Any: 194 | # 检查会话是否已初始化 195 | if not self.session: 196 | raise RuntimeError(f"Server {self.name} not initialized") 197 | 198 | # 循环尝试执行工具 199 | # 如果成功,返回执行结果 200 | attempt = 0 201 | while attempt < retries: 202 | try: 203 | logging.info(f"Executing {resource_uri}...") 204 | result = await self.session.read_resource(resource_uri) 205 | 206 | return result 207 | 208 | # 捕获异常时记录日志 209 | # 如果未超出最大重试次数,延迟后重试 210 | # 达到最大重试次数时抛出异常 211 | except Exception as e: 212 | attempt += 1 213 | logging.warning(f"Error executing resource: {e}. Attempt {attempt} of {retries}.") 214 | if attempt < retries: 215 | logging.info(f"Retrying in {delay} seconds...") 216 | await asyncio.sleep(delay) 217 | else: 218 | logging.error("Max retries reached. Failing.") 219 | raise 220 | 221 | # 清理服务器资源,确保资源释放 222 | async def cleanup(self) -> None: 223 | # 使用异步锁确保清理操作的线程安全 224 | # 清理会话资源,记录可能的警告 225 | async with self._cleanup_lock: 226 | try: 227 | if self.session: 228 | try: 229 | await self.session.__aexit__(None, None, None) 230 | except Exception as e: 231 | logging.warning(f"Warning during session cleanup for {self.name}: {e}") 232 | finally: 233 | self.session = None 234 | 235 | # 清理标准输入/输出上下文资源,捕获并记录不同类型的异常 236 | if self.stdio_context: 237 | try: 238 | await self.stdio_context.__aexit__(None, None, None) 239 | except (RuntimeError, asyncio.CancelledError) as e: 240 | logging.info(f"Note: Normal shutdown message for {self.name}: {e}") 241 | except Exception as e: 242 | logging.warning(f"Warning during stdio cleanup for {self.name}: {e}") 243 | finally: 244 | self.stdio_context = None 245 | # 捕获清理过程中可能的异常并记录错误日志 246 | except Exception as e: 247 | logging.error(f"Error during cleanup of server {self.name}: {e}") 248 | 249 | 250 | # 代表各个资源及其属性和格式 251 | class Resource: 252 | # 构造函数,在类实例化时调用 253 | # uri: 表的唯一资源标识符 254 | # name: 资源的名称 255 | # mimeType: MIME 类型,表示资源的数据类型 256 | # description: 描述信息 257 | def __init__(self, uri: str, name: str, description: str, mimeType: str) -> None: 258 | self.uri: str = uri 259 | self.name: str = name 260 | self.description: str = description 261 | self.mimeType: str = mimeType 262 | 263 | # 将资源的信息格式化为一个字符串,适合语言模型(LLM)使用 264 | def format_for_llm(self) -> str: 265 | return f""" 266 | URI: {self.uri} 267 | Name: {self.name} 268 | Description: {self.description} 269 | MimeType: {self.mimeType} 270 | """ 271 | 272 | 273 | # 代表各个工具及其属性和格式 274 | class Tool: 275 | # 构造函数,在类实例化时调用 276 | # name: 工具的名称 277 | # description: 工具的描述信息 278 | # input_schema: 工具的输入架构,通常是一个描述输入参数的字典 279 | def __init__(self, name: str, description: str, input_schema: Dict[str, Any]) -> None: 280 | self.name: str = name 281 | self.description: str = description 282 | self.input_schema: Dict[str, Any] = input_schema 283 | 284 | # 将工具的信息格式化为一个字符串,适合语言模型(LLM)使用 285 | # 返回值: 包含工具名称、描述和参数信息的格式化字符串 286 | def format_for_llm(self) -> str: 287 | args_desc = [] 288 | if 'properties' in self.input_schema: 289 | for param_name, param_info in self.input_schema['properties'].items(): 290 | arg_desc = f"- {param_name}: {param_info.get('description', 'No description')}" 291 | if param_name in self.input_schema.get('required', []): 292 | arg_desc += " (required)" 293 | args_desc.append(arg_desc) 294 | 295 | return f""" 296 | Tool: {self.name} 297 | Description: {self.description} 298 | Arguments: 299 | {chr(10).join(args_desc)} 300 | """ 301 | 302 | 303 | # 管理与LLM的通信 304 | class LLMClient: 305 | def __init__(self, base_url: str, api_key: str, chat_model: str) -> None: 306 | self.base_url: str = base_url 307 | self.api_key: str = api_key 308 | self.chat_model: str = chat_model 309 | 310 | # 向 LLM 发送请求,并返回其响应 311 | # messages: 一个字典列表,每个字典包含消息内容,通常是聊天对话的一部分 312 | # 返回值: 返回 LLM 的响应内容,类型为字符串 313 | # 如果请求失败,抛出 RequestException 314 | def get_response(self, messages: List[Dict[str, str]]) -> str: 315 | # 指定 LLM 提供者的 API 端点,用于发送聊天请求 316 | url = self.base_url 317 | 318 | # Content-Type: 表示请求体的格式为 JSON 319 | # Authorization: 使用 Bearer 令牌进行身份验证,令牌为 self.api_key 320 | headers = { 321 | "Content-Type": "application/json", 322 | "Authorization": f"Bearer {self.api_key}" 323 | } 324 | 325 | # 请求体数据,以字典形式表示 326 | # "messages": 包含传入的消息列表 327 | # "model": 使用的模型名称(例如 llama-3.2-90b-vision-preview) 328 | # "temperature": 控制生成的文本的随机性,0.7 表示适度随机 329 | # "max_tokens": 最大的输出 token 数量(4096),限制响应的长度 330 | # "top_p": 控制响应中最可能的 token 的累积概率,1 表示使用所有候选词 331 | payload = { 332 | "messages": messages, 333 | "temperature": 1.0, 334 | "top_p": 1.0, 335 | "max_tokens": 4000, 336 | "model": self.chat_model 337 | } 338 | 339 | try: 340 | # 使用 requests.post 发送 POST 请求,传递 URL、请求头和负载 341 | # url: 请求的 API 端点 342 | # headers: 请求头,包含 Content-Type 和 Authorizatio 343 | # json=payload: 将 payload 作为 JSON 格式传递 344 | response = requests.post(url, headers=headers, json=payload) 345 | # 如果响应的状态码表示错误(例如 4xx 或 5xx),则抛出异常 346 | response.raise_for_status() 347 | # 解析响应为 JSON 格式的数据 348 | data = response.json() 349 | # 从 JSON 响应中提取工具的输出内容 350 | return data['choices'][0]['message']['content'] 351 | 352 | # 如果请求失败(如连接错误、超时或无效响应等),捕获 RequestException 异常 353 | # 记录错误信息,str(e) 提供异常的具体描述 354 | except requests.exceptions.RequestException as e: 355 | error_message = f"Error getting LLM response: {str(e)}" 356 | logging.error(error_message) 357 | # 如果异常中包含响应对象(e.response),进一步记录响应的状态码和响应内容 358 | # 这有助于分析请求失败的原因(例如服务端错误、API 限制等) 359 | if e.response is not None: 360 | status_code = e.response.status_code 361 | logging.error(f"Status code: {status_code}") 362 | logging.error(f"Response details: {e.response.text}") 363 | # 返回一个友好的错误消息给调用者,告知发生了错误并建议重试或重新措辞请求 364 | return f"I encountered an error: {error_message}. Please try again or rephrase your request." 365 | 366 | 367 | # 协调用户、 LLM和工具之间的交互 368 | class ChatSession: 369 | # servers: 一个 Server 类的列表,表示多个服务器的实例 370 | # llm_client: LLMClient 的实例,用于与 LLM 进行通信 371 | def __init__(self, servers: List[Server], llm_client: LLMClient) -> None: 372 | self.servers: List[Server] = servers 373 | self.llm_client: LLMClient = llm_client 374 | 375 | # 清理所有服务器 376 | # 创建清理任务: 遍历所有的服务器实例,为每个服务器创建一个清理任务(调用 server.cleanup()) 377 | # 并行执行清理任务: 使用 asyncio.gather 并发执行所有清理任务 378 | # return_exceptions=True 表示即使部分任务抛出异常,也会继续执行其它任务 379 | # 异常处理: 如果有异常,记录警告日志 380 | async def cleanup_servers(self) -> None: 381 | cleanup_tasks = [] 382 | for server in self.servers: 383 | cleanup_tasks.append(asyncio.create_task(server.cleanup())) 384 | 385 | if cleanup_tasks: 386 | try: 387 | await asyncio.gather(*cleanup_tasks, return_exceptions=True) 388 | except Exception as e: 389 | logging.warning(f"Warning during final cleanup: {e}") 390 | 391 | # 负责处理从 LLM 返回的响应,并在需要时执行工具 392 | async def process_llm_response(self, llm_response: str) -> str: 393 | try: 394 | # 尝试将 LLM 响应解析为 JSON ,以便检查是否包含 tool 和 arguments 字段 395 | llm_call = json.loads(llm_response) 396 | 397 | # 1、如果响应包含工具名称和参数,执行相应工具 398 | if "tool" in llm_call and "arguments" in llm_call: 399 | logging.info(f"Executing tool: {llm_call['tool']}") 400 | logging.info(f"With arguments: {llm_call['arguments']}") 401 | # 遍历每个服务器,检查是否有与响应中的工具名称匹配的工具 402 | for server in self.servers: 403 | try: 404 | tools = await server.list_tools() 405 | # 如果找到相应的工具 406 | if any(tool.name == llm_call["tool"] for tool in tools): 407 | try: 408 | # 调用 server.execute_tool 来执行该工具 409 | result = await server.execute_tool(llm_call["tool"], llm_call["arguments"]) 410 | # 返回工具的执行结果 411 | return f"Tool execution result: {result}" 412 | # 如果无法找到指定的工具或遇到错误,返回相应的错误信息 413 | except Exception as e: 414 | error_msg = f"Error executing tool: {str(e)}" 415 | logging.error(error_msg) 416 | return error_msg 417 | except Exception as e: 418 | logging.error(f"Server {server.name} does not have 'list_tools' method: {e}") 419 | continue 420 | return f"No server found with tool: {llm_call['tool']}" 421 | 422 | # 2、如果响应包含资源名称和URI,执行读取相应的资源 423 | if "resource" in llm_call : 424 | logging.info(f"Executing resource: {llm_call['resource']}") 425 | logging.info(f"With URI: {llm_call['uri']}") 426 | # 遍历每个服务器,检查是否有与响应中的资源名称匹配的资源 427 | for server in self.servers: 428 | try: 429 | resources = await server.list_resources() 430 | # 如果找到相应的资源 431 | if any(resource.uri == llm_call["uri"] for resource in resources): 432 | try: 433 | # 调用 server.read_resource 来读取该资源 434 | result = await server.read_resource(llm_call["uri"]) 435 | # 返回资源的执行结果 436 | return f"Resource execution result: {result}" 437 | # 如果无法找到指定的资源或遇到错误,返回相应的错误信息 438 | except Exception as e: 439 | error_msg = f"Error executing resource: {str(e)}" 440 | logging.error(error_msg) 441 | return error_msg 442 | except Exception as e: 443 | logging.error(f"Server {server.name} does not have 'list_resources' method: {e}") 444 | continue 445 | return f"No server found with resource: {llm_call['uri']}" 446 | return llm_response 447 | 448 | # 如果响应无法解析为 JSON,返回原始 LLM 响应 449 | except json.JSONDecodeError: 450 | return llm_response 451 | 452 | # 方法: start 用于启动整个聊天会话,初始化服务器并开始与用户的互动 453 | async def start(self) -> None: 454 | try: 455 | # 初始化所有服务器: 遍历 self.servers,异步调用每个服务器的 initialize 方法 456 | for server in self.servers: 457 | try: 458 | await server.initialize() 459 | # 异常处理: 如果服务器初始化失败,记录错误并调用 cleanup_servers 清理资源,然后退出会话 460 | except Exception as e: 461 | logging.error(f"Failed to initialize server: {e}") 462 | await self.cleanup_servers() 463 | return 464 | 465 | # 遍历所有服务器,调用 list_tools() 获取每个服务器的工具列表 466 | all_tools = [] 467 | for server in self.servers: 468 | try: 469 | tools = await server.list_tools() 470 | all_tools.extend(tools) 471 | except Exception as e: 472 | print(f"Server {server.name} does not have 'list_tools' method: {e}") 473 | # 将所有工具的描述信息汇总,生成供 LLM 使用的工具描述字符串 474 | try: 475 | tools_description = "\n".join([tool.format_for_llm() for tool in all_tools]) 476 | except Exception as e: 477 | print(f"Error while formatting resources for LLM: {e}") 478 | tools_description = "" 479 | 480 | # # 遍历所有服务器,调用 list_resources() 获取每个服务器的资源列表 481 | all_resources = [] 482 | for server in self.servers: 483 | try: 484 | resources = await server.list_resources() 485 | all_resources.extend(resources) 486 | except Exception as e: 487 | print(f"Server {server.name} does not have 'list_resources' method: {e}") 488 | # 将所有资源的描述信息汇总,生成供 LLM 使用的资源描述字符串 489 | try: 490 | resources_description = "\n".join([resource.format_for_llm() for resource in all_resources]) 491 | except Exception as e: 492 | print(f"Error while formatting resources for LLM: {e}") 493 | resources_description = "" 494 | 495 | 496 | # 构建一个系统消息,作为 LLM 交互的指令,告知 LLM 使用哪些工具以及如何与用户进行交互 497 | # 系统消息强调 LLM 必须以严格的 JSON 格式请求工具,并且在工具响应后将其转换为自然语言响应 498 | system_message = f"""You are a helpful assistant with access to these resources and tools: 499 | 500 | resources:{resources_description} 501 | tools:{tools_description} 502 | Choose the appropriate resource or tool based on the user's question. If no resource or tool is needed, reply directly. 503 | 504 | IMPORTANT: When you need to use a resource, you must ONLY respond with the exact JSON object format below, nothing else: 505 | {{ 506 | "resource": "resource-name", 507 | "uri": "resource-URI" 508 | }} 509 | 510 | After receiving a resource's response: 511 | 1. Transform the raw data into a natural, conversational response 512 | 2. Keep responses concise but informative 513 | 3. Focus on the most relevant information 514 | 4. Use appropriate context from the user's question 515 | 5. Avoid simply repeating the raw data 516 | 517 | IMPORTANT: When you need to use a tool, you must ONLY respond with the exact JSON object format below, nothing else: 518 | {{ 519 | "tool": "tool-name", 520 | "arguments": {{ 521 | "argument-name": "value" 522 | }} 523 | }} 524 | 525 | After receiving a tool's response: 526 | 1. Transform the raw data into a natural, conversational response 527 | 2. Keep responses concise but informative 528 | 3. Focus on the most relevant information 529 | 4. Use appropriate context from the user's question 530 | 5. Avoid simply repeating the raw data 531 | 532 | Please use only the resources or tools that are explicitly defined above.""" 533 | 534 | # 消息初始化 创建一个消息列表,其中包含一个系统消息,指示 LLM 如何与用户交互 535 | messages = [ 536 | { 537 | "role": "system", 538 | "content": system_message 539 | } 540 | ] 541 | 542 | # 交互循环 543 | while True: 544 | try: 545 | # 等待用户输入,如果用户输入 quit 或 exit,则退出 546 | user_input = input("user: ").strip().lower() 547 | if user_input in ['quit', 'exit']: 548 | logging.info("\nExiting...") 549 | break 550 | 551 | # 将用户输入添加到消息列表 552 | messages.append({"role": "user", "content": user_input}) 553 | 554 | # 调用 LLM 客户端获取 LLM 的响应 555 | llm_response = self.llm_client.get_response(messages) 556 | logging.info("\nAssistant: %s", llm_response) 557 | 558 | # 调用 process_llm_response 方法处理 LLM 响应,执行工具(如果需要) 559 | result = await self.process_llm_response(llm_response) 560 | 561 | # 如果工具执行结果与 LLM 响应不同,更新消息列表并再次与 LLM 交互,获取最终响应 562 | if result != llm_response: 563 | messages.append({"role": "assistant", "content": llm_response}) 564 | messages.append({"role": "system", "content": result}) 565 | 566 | final_response = self.llm_client.get_response(messages) 567 | logging.info("\nFinal response: %s", final_response) 568 | messages.append({"role": "assistant", "content": final_response}) 569 | else: 570 | messages.append({"role": "assistant", "content": llm_response}) 571 | 572 | # 处理 KeyboardInterrupt,允许用户中断会话 573 | except KeyboardInterrupt: 574 | logging.info("\nExiting...") 575 | break 576 | # 清理资源: 无论会话如何结束(正常结束或由于异常退出),都确保调用 cleanup_servers 清理服务器资源 577 | finally: 578 | await self.cleanup_servers() 579 | 580 | 581 | async def main() -> None: 582 | # 创建一个 Configuration 类的实例 583 | config = Configuration() 584 | # 调用 Configuration 类中的 load_config 方法来加载配置文件 servers_config.json 585 | server_config = config.load_config('servers_config.json') 586 | # 遍历 server_config['mcpServers'] 字典,并为每个服务器创建一个 Server 实例,传入服务器名称 (name) 和配置信息 (srv_config) 587 | # 结果是一个 Server 对象的列表,保存在 servers 变量中 588 | servers = [Server(name, srv_config) for name, srv_config in server_config['mcpServers'].items()] 589 | # 创建一个 LLMClient 实例,用于与 LLM (大语言模型) 进行交互 590 | llm_client = LLMClient(config.llm_base_url, config.llm_api_key, config.llm_chat_model) 591 | # 创建一个 ChatSession 实例,负责管理与用户的聊天交互、LLM 响应和工具执行 592 | # 将之前创建的 servers 和 llm_client 传递给 ChatSession 构造函数,初始化会话 593 | chat_session = ChatSession(servers, llm_client) 594 | # 调用 ChatSession 类的 start 方法,启动聊天会话 595 | # 由于 start 是一个异步方法,所以使用 await 等待该方法执行完毕 596 | # start 方法将处理用户的输入、与 LLM 交互、执行工具(如果需要)等,并持续运行直到用户选择退出 597 | await chat_session.start() 598 | 599 | 600 | 601 | 602 | if __name__ == "__main__": 603 | asyncio.run(main()) 604 | 605 | --------------------------------------------------------------------------------