From 8504c181c0b89f7876e9653a5012a4c29ef33584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=84=E5=87=A4=E7=A7=91=E6=8A=80?= Date: Thu, 20 Mar 2025 08:59:45 +0800 Subject: [PATCH 1/8] =?UTF-8?q?mcp=E5=88=9D=E6=AD=A5=E8=B0=83=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/connection.py | 31 +++++- main/xiaozhi-server/core/mcp/MCPClient.py | 96 +++++++++++++++++ main/xiaozhi-server/core/mcp/manager.py | 102 ++++++++++++++++++ .../plugins_func/functions/get_time.py | 2 +- main/xiaozhi-server/plugins_func/register.py | 1 + 5 files changed, 228 insertions(+), 4 deletions(-) create mode 100644 main/xiaozhi-server/core/mcp/MCPClient.py create mode 100644 main/xiaozhi-server/core/mcp/manager.py diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index 718398c3..b5e7dcf2 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -22,6 +22,7 @@ from plugins_func.register import Action from config.private_config import PrivateConfig from core.auth import AuthMiddleware, AuthenticationError from core.utils.auth_code_gen import AuthCodeGenerator +from core.mcp.manager import MCPManager TAG = __name__ @@ -98,6 +99,8 @@ class ConnectionHandler: self.use_function_call_mode = False if self.config["selected_module"]["Intent"] == 'function_call': self.use_function_call_mode = True + + self.mcp_manager = MCPManager(self) async def handle_connection(self, ws): try: @@ -151,6 +154,8 @@ class ConnectionHandler: # 异步初始化 await self.loop.run_in_executor(None, self._initialize_components) + # 初始化MCP服务 + await self.mcp_manager.initialize_servers() # tts 消化线程 tts_priority = threading.Thread(target=self._tts_priority_thread, daemon=True) @@ -317,7 +322,9 @@ class ConnectionHandler: # Define intent functions functions = self.func_handler.get_functions() - + mcpfuncs = self.mcp_manager.get_all_tools() # MCP工具 + functions.extend(mcpfuncs) + print("functions with mcp", functions) response_message = [] processed_chars = 0 # 跟踪已处理的字符位置 @@ -429,8 +436,24 @@ class ConnectionHandler: "id": function_id, "arguments": function_arguments } - result = self.func_handler.handle_llm_function_call(self, function_call_data) - self._handle_function_result(result, function_call_data, text_index + 1) + #result = self.func_handler.handle_llm_function_call(self, function_call_data) + #self._handle_function_result(result, function_call_data, text_index+1) + + # 处理MCP工具调用 + if self.mcp_manager.is_mcp_tool(function_name): + try: + tool_result = asyncio.create_task(self.mcp_manager.execute_tool( + function_name, + function_arguments + )) + self._handle_mcp_tool_result(tool_result, function_call_data, text_index+1) + except Exception as e: + self.logger.bind(tag=TAG).error(f"MCP工具调用错误: {e}") + response_message.append(f"MCP工具调用失败: {str(e)}") + else: + # 处理系统函数 + result = self.func_handler.handle_llm_function_call(self, function_call_data) + self._handle_function_result(result, function_call_data, text_index+1) # 处理最后剩余的文本 full_text = "".join(response_message) @@ -565,6 +588,8 @@ class ConnectionHandler: async def close(self): """资源清理方法""" + # 清理MCP资源 + await self.mcp_manager.cleanup_all() # 清理其他资源 self.stop_event.set() diff --git a/main/xiaozhi-server/core/mcp/MCPClient.py b/main/xiaozhi-server/core/mcp/MCPClient.py new file mode 100644 index 00000000..903ce60b --- /dev/null +++ b/main/xiaozhi-server/core/mcp/MCPClient.py @@ -0,0 +1,96 @@ +import asyncio +from typing import Optional +from contextlib import AsyncExitStack +import os +from mcp import ClientSession, StdioServerParameters +from mcp.client.stdio import stdio_client + +from config.logger import setup_logging + +TAG = __name__ + +class MCPClient: + def __init__(self, config): + # Initialize session and client objects + self.session: Optional[ClientSession] = None + self.exit_stack = AsyncExitStack() + self.logger = setup_logging() + self.config = config + self.tolls = [] + + async def initialize(self): + main_command = self.config["command"] + args = self.config.get("args", []) + + server_script_path = main_command + if args: + # 如果第一个参数是路径,使用其目录作为工作目录 + possible_path = args[0] + if os.path.exists(possible_path): + server_script_path = possible_path + await self.connect_to_server(server_script_path) + + + async def connect_to_server(self, server_script_path: str): + """Connect to an MCP server + + Args: + server_script_path: Path to the server script (.py or .js) + """ + is_python = server_script_path.endswith('.py') + is_js = server_script_path.endswith('.js') + if not (is_python or is_js): + raise ValueError("Server script must be a .py or .js file") + + env = self.config.get("env", {}) + command = "python" if is_python else "node" + server_params = StdioServerParameters( + command=command, + args=[server_script_path], + env=env + ) + + stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params)) + self.stdio, self.write = stdio_transport + self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write)) + + await self.session.initialize() + + # List available tools + response = await self.session.list_tools() + tools = response.tools + self.tools = tools + self.logger.bind(tag=TAG).info(f"Connected to server with tools:{[tool.name for tool in tools]}") + + def get_available_tools(self): + available_tools = [{"type": "function", "function":{ + "name": tool.name, + "description": tool.description, + "parameters": tool.inputSchema + } } for tool in self.tools] + + return available_tools + + async def call_tool(self, tool_name: str, tool_args: dict): + response = await self.session.call_tool(tool_name, tool_args) + return response + + async def cleanup(self): + """Clean up resources""" + await self.exit_stack.aclose() + +async def main(): + if len(sys.argv) < 2: + print("Usage: python client.py ") + sys.exit(1) + + client = MCPClient() + try: + await client.connect_to_server(sys.argv[1]) + await client.chat_loop() + finally: + await client.cleanup() + +if __name__ == "__main__": + import sys + asyncio.run(main()) \ No newline at end of file diff --git a/main/xiaozhi-server/core/mcp/manager.py b/main/xiaozhi-server/core/mcp/manager.py new file mode 100644 index 00000000..f9b4a742 --- /dev/null +++ b/main/xiaozhi-server/core/mcp/manager.py @@ -0,0 +1,102 @@ +"""MCP服务管理器""" +import json +from typing import Dict, Any, List +from .MCPClient import MCPClient +from config.logger import setup_logging +from core.utils.util import get_project_dir +from plugins_func.register import register_function, ActionResponse, Action, ToolType + +TAG = __name__ + +class MCPManager: + """管理多个MCP服务的集中管理器""" + + def __init__(self,conn) -> None: + """ + 初始化MCP管理器 + """ + self.conn = conn + self.config_path = get_project_dir() + 'data/.mcp_server_settings.json' + self.client: Dict[str, MCPClient] = {} + self.logger = setup_logging() + self.tools = [] + + def load_config(self) -> Dict[str, Any]: + """加载MCP服务配置 + Returns: + Dict[str, Any]: 服务配置字典 + """ + try: + with open(self.config_path, 'r', encoding='utf-8') as f: + config = json.load(f) + return config.get('mcpServers', {}) + except Exception as e: + self.logger.bind(tag=TAG).error(f"Error loading MCP config from {self.config_path}: {e}") + return {} + + async def initialize_servers(self) -> None: + """初始化所有MCP服务""" + config = self.load_config() + for name, srv_config in config.items(): + if not srv_config.get("command"): + self.logger.bind(tag=TAG).warning(f"Skipping server {name}: command not specified") + continue + + try: + client = MCPClient(srv_config) + await client.initialize() + self.client[name] = client + self.logger.bind(tag=TAG).info(f"Initialized MCP client: {name}") + client_tools = client.get_available_tools() + self.tools.extend(client_tools) + for tool in client_tools: + func_name = "mcp_"+tool["function"]["name"] + register_function(func_name, tool, ToolType.MCP_CLIENT)(self.execute_tool) + self.conn.func_handler.function_registry.register_function(func_name) + + except Exception as e: + self.logger.bind(tag=TAG).error(f"Failed to initialize MCP server {name}: {e}") + + def get_all_tools(self) -> List[Dict[str, Any]]: + """获取所有服务的工具function定义 + Returns: + List[Dict[str, Any]]: 所有工具的function定义列表 + """ + return self.tools + + def is_mcp_tool(self, tool_name: str) -> bool: + """检查是否是MCP工具 + Args: + tool_name: 工具名称 + Returns: + bool: 是否是MCP工具 + """ + for tool in self.tools: + if tool.get("function") != None and tool["function"].get("name") == tool_name: + return True + return False + + async def execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any: + """执行工具调用 + Args: + tool_name: 工具名称 + arguments: 工具参数 + Returns: + Any: 工具执行结果 + Raises: + ValueError: 工具未找到时抛出 + """ + for client in self.client.values(): + if any(tool_name == tool["name"] for tool in client.tools): + return await client.call_tool(tool_name, arguments) + + raise ValueError(f"Tool {tool_name} not found in any MCP server") + + async def cleanup_all(self) -> None: + for name, client in self.client.items(): + try: + await client.cleanup() + self.logger.bind(tag=TAG).info(f"Cleaned up MCP client: {name}") + except Exception as e: + self.logger.bind(tag=TAG).error(f"Error cleaning up MCP client {name}: {e}") + self.client.clear() diff --git a/main/xiaozhi-server/plugins_func/functions/get_time.py b/main/xiaozhi-server/plugins_func/functions/get_time.py index 1d141ed2..9a9aebfe 100644 --- a/main/xiaozhi-server/plugins_func/functions/get_time.py +++ b/main/xiaozhi-server/plugins_func/functions/get_time.py @@ -6,7 +6,7 @@ get_time_function_desc = { "function": { "name": "get_time", "description": "获取当前时间、日期、星期几", - "parameters": {} + 'parameters': {'type': 'object', 'properties': {}, 'required': []} } } diff --git a/main/xiaozhi-server/plugins_func/register.py b/main/xiaozhi-server/plugins_func/register.py index ccb03b44..fd990760 100644 --- a/main/xiaozhi-server/plugins_func/register.py +++ b/main/xiaozhi-server/plugins_func/register.py @@ -12,6 +12,7 @@ class ToolType(Enum): CHANGE_SYS_PROMPT = (3, "修改系统提示词,切换角色性格或职责") SYSTEM_CTL = (4, "系统控制,影响正常的对话流程,如退出、播放音乐等,需要传递conn参数") IOT_CTL = (5, "IOT设备控制,需要传递conn参数") + MCP_CLIENT = (6, "MCP客户端") def __init__(self, code, message): self.code = code From de9809ca5aa15ba25440e7a5282eef0f6b20325f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=84=E5=87=A4=E7=A7=91=E6=8A=80?= Date: Thu, 20 Mar 2025 09:06:36 +0800 Subject: [PATCH 2/8] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E5=AF=BC=E8=87=B4chatglm=E5=8D=A1=E4=BD=8F?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/plugins_func/functions/get_time.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/xiaozhi-server/plugins_func/functions/get_time.py b/main/xiaozhi-server/plugins_func/functions/get_time.py index 1d141ed2..9a9aebfe 100644 --- a/main/xiaozhi-server/plugins_func/functions/get_time.py +++ b/main/xiaozhi-server/plugins_func/functions/get_time.py @@ -6,7 +6,7 @@ get_time_function_desc = { "function": { "name": "get_time", "description": "获取当前时间、日期、星期几", - "parameters": {} + 'parameters': {'type': 'object', 'properties': {}, 'required': []} } } From 2b81ebca8e3bbebef5c90c015f58d4ef355bb1b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=84=E5=87=A4=E7=A7=91=E6=8A=80?= Date: Thu, 20 Mar 2025 11:52:37 +0800 Subject: [PATCH 3/8] =?UTF-8?q?=E8=B0=83=E9=80=9Amcp=20tool?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/connection.py | 52 ++++++++++++++------- main/xiaozhi-server/core/mcp/MCPClient.py | 55 ++++++----------------- main/xiaozhi-server/core/mcp/manager.py | 5 ++- 3 files changed, 53 insertions(+), 59 deletions(-) diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index b5e7dcf2..088598e2 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -18,7 +18,7 @@ from concurrent.futures import ThreadPoolExecutor, TimeoutError from core.handle.sendAudioHandle import sendAudioMessage from core.handle.receiveAudioHandle import handleAudioMessage from core.handle.functionHandler import FunctionHandler -from plugins_func.register import Action +from plugins_func.register import Action, ActionResponse from config.private_config import PrivateConfig from core.auth import AuthMiddleware, AuthenticationError from core.utils.auth_code_gen import AuthCodeGenerator @@ -196,9 +196,9 @@ class ConnectionHandler: if self.private_config: self.prompt = self.private_config.private_config.get("prompt", self.prompt) - self.client_ip_info = get_ip_info(self.client_ip) - self.logger.bind(tag=TAG).info(f"Client ip info: {self.client_ip_info}") - self.prompt = self.prompt + f"\n我在:{self.client_ip_info}" + #self.client_ip_info = get_ip_info(self.client_ip) + #self.logger.bind(tag=TAG).info(f"Client ip info: {self.client_ip_info}") + #self.prompt = self.prompt + f"\n我在:{self.client_ip_info}" self.dialogue.put(Message(role="system", content=self.prompt)) self.func_handler = FunctionHandler(self.config) @@ -436,24 +436,14 @@ class ConnectionHandler: "id": function_id, "arguments": function_arguments } - #result = self.func_handler.handle_llm_function_call(self, function_call_data) - #self._handle_function_result(result, function_call_data, text_index+1) # 处理MCP工具调用 if self.mcp_manager.is_mcp_tool(function_name): - try: - tool_result = asyncio.create_task(self.mcp_manager.execute_tool( - function_name, - function_arguments - )) - self._handle_mcp_tool_result(tool_result, function_call_data, text_index+1) - except Exception as e: - self.logger.bind(tag=TAG).error(f"MCP工具调用错误: {e}") - response_message.append(f"MCP工具调用失败: {str(e)}") + result = self._handle_mcp_tool_call(function_call_data) else: # 处理系统函数 result = self.func_handler.handle_llm_function_call(self, function_call_data) - self._handle_function_result(result, function_call_data, text_index+1) + self._handle_function_result(result, function_call_data, text_index+1) # 处理最后剩余的文本 full_text = "".join(response_message) @@ -475,6 +465,36 @@ class ConnectionHandler: return True + def _handle_mcp_tool_call(self, function_call_data): + function_arguments = function_call_data["arguments"] + function_name = function_call_data["name"] + try: + args_dict = function_arguments + if isinstance(function_arguments, str): + try: + args_dict = json.loads(function_arguments) + except json.JSONDecodeError: + self.logger.bind(tag=TAG).error(f"无法解析 function_arguments: {function_arguments}") + return ActionResponse(action=Action.REQLLM, result="参数解析失败", response="") + + tool_result = asyncio.run_coroutine_threadsafe(self.mcp_manager.execute_tool( + function_name, + args_dict + ), self.loop).result() + # meta=None content=[TextContent(type='text', text='北京当前天气:\n温度: 21°C\n天气: 晴\n湿度: 6%\n风向: 西北 风\n风力等级: 5级', annotations=None)] isError=False + self.logger.bind(tag=TAG).info(f"tool_result:{tool_result}") + if tool_result is not None: + if tool_result.content is not None: + print(tool_result.content) + return ActionResponse(action=Action.REQLLM, result=tool_result.content[0].text, response="") + + except Exception as e: + self.logger.bind(tag=TAG).error(f"MCP工具调用错误: {e}") + return ActionResponse(action=Action.REQLLM, result="工具调用出错", response="") + + return ActionResponse(action=Action.REQLLM, result="工具调用出错", response="") + + def _handle_function_result(self, result, function_call_data, text_index): if result.action == Action.RESPONSE: # 直接回复前端 text = result.response diff --git a/main/xiaozhi-server/core/mcp/MCPClient.py b/main/xiaozhi-server/core/mcp/MCPClient.py index 903ce60b..a8be61e1 100644 --- a/main/xiaozhi-server/core/mcp/MCPClient.py +++ b/main/xiaozhi-server/core/mcp/MCPClient.py @@ -1,7 +1,7 @@ import asyncio from typing import Optional from contextlib import AsyncExitStack -import os +import os, shutil from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client @@ -19,34 +19,19 @@ class MCPClient: self.tolls = [] async def initialize(self): - main_command = self.config["command"] args = self.config.get("args", []) - server_script_path = main_command - if args: - # 如果第一个参数是路径,使用其目录作为工作目录 - possible_path = args[0] - if os.path.exists(possible_path): - server_script_path = possible_path - await self.connect_to_server(server_script_path) + command = ( + shutil.which("npx") + if self.config["command"] == "npx" + else self.config["command"] + ) - - async def connect_to_server(self, server_script_path: str): - """Connect to an MCP server + env={**os.environ, **self.config["env"]} - Args: - server_script_path: Path to the server script (.py or .js) - """ - is_python = server_script_path.endswith('.py') - is_js = server_script_path.endswith('.js') - if not (is_python or is_js): - raise ValueError("Server script must be a .py or .js file") - - env = self.config.get("env", {}) - command = "python" if is_python else "node" server_params = StdioServerParameters( command=command, - args=[server_script_path], + args=args, env=env ) @@ -61,7 +46,10 @@ class MCPClient: tools = response.tools self.tools = tools self.logger.bind(tag=TAG).info(f"Connected to server with tools:{[tool.name for tool in tools]}") - + + def has_tool(self, tool_name): + return any(tool.name == tool_name for tool in self.tools) + def get_available_tools(self): available_tools = [{"type": "function", "function":{ "name": tool.name, @@ -72,25 +60,10 @@ class MCPClient: return available_tools async def call_tool(self, tool_name: str, tool_args: dict): + self.logger.bind(tag=TAG).info(f"MCPClient Calling tool {tool_name} with args: {tool_args}") response = await self.session.call_tool(tool_name, tool_args) return response async def cleanup(self): """Clean up resources""" - await self.exit_stack.aclose() - -async def main(): - if len(sys.argv) < 2: - print("Usage: python client.py ") - sys.exit(1) - - client = MCPClient() - try: - await client.connect_to_server(sys.argv[1]) - await client.chat_loop() - finally: - await client.cleanup() - -if __name__ == "__main__": - import sys - asyncio.run(main()) \ No newline at end of file + await self.exit_stack.aclose() \ No newline at end of file diff --git a/main/xiaozhi-server/core/mcp/manager.py b/main/xiaozhi-server/core/mcp/manager.py index f9b4a742..cae3bcf2 100644 --- a/main/xiaozhi-server/core/mcp/manager.py +++ b/main/xiaozhi-server/core/mcp/manager.py @@ -80,14 +80,15 @@ class MCPManager: """执行工具调用 Args: tool_name: 工具名称 - arguments: 工具参数 + arguments: 工具参数 Returns: Any: 工具执行结果 Raises: ValueError: 工具未找到时抛出 """ + self.logger.bind(tag=TAG).info(f"Executing tool {tool_name} with arguments: {arguments}") for client in self.client.values(): - if any(tool_name == tool["name"] for tool in client.tools): + if client.has_tool(tool_name): return await client.call_tool(tool_name, arguments) raise ValueError(f"Tool {tool_name} not found in any MCP server") From 898fd36eca4e3e5b78b4325b8a03038627742f80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=84=E5=87=A4=E7=A7=91=E6=8A=80?= Date: Thu, 20 Mar 2025 18:20:09 +0800 Subject: [PATCH 4/8] =?UTF-8?q?=E5=A2=9E=E5=8A=A0mcp=E8=B0=83=E7=94=A8?= =?UTF-8?q?=E8=B6=85=E6=97=B6=E6=A3=80=E6=B5=8B=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/connection.py | 19 +++++++++------- main/xiaozhi-server/core/mcp/MCPClient.py | 27 ++++++++++++++++++----- main/xiaozhi-server/core/mcp/manager.py | 12 +++++++--- 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index 088598e2..e536d00e 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -322,9 +322,6 @@ class ConnectionHandler: # Define intent functions functions = self.func_handler.get_functions() - mcpfuncs = self.mcp_manager.get_all_tools() # MCP工具 - functions.extend(mcpfuncs) - print("functions with mcp", functions) response_message = [] processed_chars = 0 # 跟踪已处理的字符位置 @@ -482,11 +479,17 @@ class ConnectionHandler: args_dict ), self.loop).result() # meta=None content=[TextContent(type='text', text='北京当前天气:\n温度: 21°C\n天气: 晴\n湿度: 6%\n风向: 西北 风\n风力等级: 5级', annotations=None)] isError=False - self.logger.bind(tag=TAG).info(f"tool_result:{tool_result}") - if tool_result is not None: - if tool_result.content is not None: - print(tool_result.content) - return ActionResponse(action=Action.REQLLM, result=tool_result.content[0].text, response="") + content_text = "" + if tool_result is not None and tool_result.content is not None: + for content in tool_result.content: + content_type = content.type + if content_type == "text": + content_text = content.text + elif content_type == "image": + pass + + if len(content_text) > 0: + return ActionResponse(action=Action.REQLLM, result=content_text, response="") except Exception as e: self.logger.bind(tag=TAG).error(f"MCP工具调用错误: {e}") diff --git a/main/xiaozhi-server/core/mcp/MCPClient.py b/main/xiaozhi-server/core/mcp/MCPClient.py index a8be61e1..3c32e077 100644 --- a/main/xiaozhi-server/core/mcp/MCPClient.py +++ b/main/xiaozhi-server/core/mcp/MCPClient.py @@ -1,4 +1,4 @@ -import asyncio +from datetime import timedelta from typing import Optional from contextlib import AsyncExitStack import os, shutil @@ -26,8 +26,10 @@ class MCPClient: if self.config["command"] == "npx" else self.config["command"] ) - - env={**os.environ, **self.config["env"]} + + env={**os.environ} + if self.config.get("env"): + env.update(self.config["env"]) server_params = StdioServerParameters( command=command, @@ -37,7 +39,8 @@ class MCPClient: stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params)) self.stdio, self.write = stdio_transport - self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write)) + time_out_delta = timedelta(seconds=10) + self.session = await self.exit_stack.enter_async_context(ClientSession(read_stream=self.stdio, write_stream=self.write, read_timeout_seconds=time_out_delta)) await self.session.initialize() @@ -61,7 +64,21 @@ class MCPClient: async def call_tool(self, tool_name: str, tool_args: dict): self.logger.bind(tag=TAG).info(f"MCPClient Calling tool {tool_name} with args: {tool_args}") - response = await self.session.call_tool(tool_name, tool_args) + try: + response = await self.session.call_tool(tool_name, tool_args) + except Exception as e: + self.logger.bind(tag=TAG).error(f"Error calling tool {tool_name}: {e}") + from types import SimpleNamespace + error_content = SimpleNamespace( + type='text', + text=f"Error calling tool {tool_name}: {e}" + ) + error_response = SimpleNamespace( + content=[error_content], + isError=True + ) + return error_response + self.logger.bind(tag=TAG).info(f"MCPClient Response from tool {tool_name}: {response}") return response async def cleanup(self): diff --git a/main/xiaozhi-server/core/mcp/manager.py b/main/xiaozhi-server/core/mcp/manager.py index cae3bcf2..810554e7 100644 --- a/main/xiaozhi-server/core/mcp/manager.py +++ b/main/xiaozhi-server/core/mcp/manager.py @@ -1,5 +1,5 @@ """MCP服务管理器""" -import json +import os, json from typing import Dict, Any, List from .MCPClient import MCPClient from config.logger import setup_logging @@ -16,9 +16,12 @@ class MCPManager: 初始化MCP管理器 """ self.conn = conn - self.config_path = get_project_dir() + 'data/.mcp_server_settings.json' - self.client: Dict[str, MCPClient] = {} self.logger = setup_logging() + self.config_path = get_project_dir() + 'data/.mcp_server_settings.json' + if os.path.exists(self.config_path) == False: + self.config_path = "" + self.logger.bind(tag=TAG).warning(f"请检查mcp服务配置文件:data/.mcp_server_settings.json") + self.client: Dict[str, MCPClient] = {} self.tools = [] def load_config(self) -> Dict[str, Any]: @@ -26,6 +29,9 @@ class MCPManager: Returns: Dict[str, Any]: 服务配置字典 """ + if len(self.config_path) == 0: + return {} + try: with open(self.config_path, 'r', encoding='utf-8') as f: config = json.load(f) From 3ea95ffa48cf17d92471734b5b4c6585eb845d4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=84=E5=87=A4=E7=A7=91=E6=8A=80?= Date: Thu, 20 Mar 2025 18:20:50 +0800 Subject: [PATCH 5/8] =?UTF-8?q?mcp=E6=A8=A1=E6=9D=BF=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/mcp_server_settings.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 main/xiaozhi-server/mcp_server_settings.json diff --git a/main/xiaozhi-server/mcp_server_settings.json b/main/xiaozhi-server/mcp_server_settings.json new file mode 100644 index 00000000..1beb79a3 --- /dev/null +++ b/main/xiaozhi-server/mcp_server_settings.json @@ -0,0 +1,16 @@ +{ + "mcpServers": { + "filesystem": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-filesystem", + "/path/to/other/allowed/dir" + ] + }, + "puppeteer": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-puppeteer"] + } + } +} From 7ead5537f622c10e5395fd2ee37f7cc71ea05601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=84=E5=87=A4=E7=A7=91=E6=8A=80?= Date: Fri, 21 Mar 2025 17:15:54 +0800 Subject: [PATCH 6/8] =?UTF-8?q?=E6=8F=90=E4=BA=A4requirements.txt=E4=B8=AD?= =?UTF-8?q?mcp=E7=89=88=E6=9C=AC=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/main/xiaozhi-server/requirements.txt b/main/xiaozhi-server/requirements.txt index 81d0cdfd..386d054c 100755 --- a/main/xiaozhi-server/requirements.txt +++ b/main/xiaozhi-server/requirements.txt @@ -20,3 +20,4 @@ requests==2.32.3 cozepy==0.12.0 mem0ai==0.1.62 bs4==0.0.2 +mcp==1.4.1 \ No newline at end of file From dc4968f9614c636eb12a234bf51356b6ee5e98c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=84=E5=87=A4=E7=A7=91=E6=8A=80?= Date: Fri, 21 Mar 2025 17:31:29 +0800 Subject: [PATCH 7/8] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E8=A1=8C=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/mcp_server_settings.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/main/xiaozhi-server/mcp_server_settings.json b/main/xiaozhi-server/mcp_server_settings.json index 1beb79a3..7e4dd5d2 100644 --- a/main/xiaozhi-server/mcp_server_settings.json +++ b/main/xiaozhi-server/mcp_server_settings.json @@ -11,6 +11,10 @@ "puppeteer": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-puppeteer"] + }, + "windows-cli": { + "command": "npx", + "args": ["-y", "@simonb97/server-win-cli"] } } } From 8c4c9d88dafeee147aa657bdc8cf1ddca51abf35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=84=E5=87=A4=E7=A7=91=E6=8A=80?= Date: Mon, 24 Mar 2025 10:28:48 +0800 Subject: [PATCH 8/8] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=BD=91=E7=AB=99?= =?UTF-8?q?=E6=B5=8F=E8=A7=88mcp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/connection.py | 3 +++ main/xiaozhi-server/core/mcp/manager.py | 1 + main/xiaozhi-server/mcp_server_settings.json | 19 +++++++++++++++---- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index 447c6f8d..4523911a 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -208,6 +208,9 @@ class ConnectionHandler: self.prompt = self.prompt + f"\nuser location:{self.client_ip_info}" self.dialogue.update_system_message(self.prompt) + """加载MCP工具""" + asyncio.run_coroutine_threadsafe(self.mcp_manager.initialize_servers(), self.loop) + def change_system_prompt(self, prompt): self.prompt = prompt # 找到原来的role==system,替换原来的系统提示 diff --git a/main/xiaozhi-server/core/mcp/manager.py b/main/xiaozhi-server/core/mcp/manager.py index 810554e7..28efd87d 100644 --- a/main/xiaozhi-server/core/mcp/manager.py +++ b/main/xiaozhi-server/core/mcp/manager.py @@ -62,6 +62,7 @@ class MCPManager: except Exception as e: self.logger.bind(tag=TAG).error(f"Failed to initialize MCP server {name}: {e}") + self.conn.func_handler.upload_functions_desc() def get_all_tools(self) -> List[Dict[str, Any]]: """获取所有服务的工具function定义 diff --git a/main/xiaozhi-server/mcp_server_settings.json b/main/xiaozhi-server/mcp_server_settings.json index 7e4dd5d2..a38bbdfb 100644 --- a/main/xiaozhi-server/mcp_server_settings.json +++ b/main/xiaozhi-server/mcp_server_settings.json @@ -1,20 +1,31 @@ { + "des": [ + "在data目录下创建.mcp_server_settings.json文件,可以选择下面的MCP服务,也可以自行添加新的MCP服务。", + "后面不断测试补充好用的mcp服务,欢迎大家一起补充。", + "记得删除注释行,des属性仅为说明,不会被解析。", + "des和link属性,仅为说明安装方式,方便大家查看原始链接,不是必须项。" + ], "mcpServers": { "filesystem": { "command": "npx", "args": [ "-y", "@modelcontextprotocol/server-filesystem", + "/Users/username/Desktop", "/path/to/other/allowed/dir" - ] + ], + "link":"https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem" }, - "puppeteer": { + "playwright": { "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-puppeteer"] + "args": ["-y", "@executeautomation/playwright-mcp-server"], + "des" : "run 'npx playwright install' first", + "link": "https://github.com/executeautomation/mcp-playwright" }, "windows-cli": { "command": "npx", - "args": ["-y", "@simonb97/server-win-cli"] + "args": ["-y", "@simonb97/server-win-cli"], + "link": "https://github.com/SimonB97/win-cli-mcp-server" } } }