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] =?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")