├── 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 |
--------------------------------------------------------------------------------