From 4d0ddd7ff3f30ffa7d83bfc97671247da68bc145 Mon Sep 17 00:00:00 2001 From: whats2000 <60466660+whats2000@users.noreply.github.com> Date: Sat, 7 Jun 2025 13:46:35 +0800 Subject: [PATCH 1/2] fix: sanitize MCP tool names for OpenAI --- main/xiaozhi-server/core/handle/mcpHandle.py | 14 ++++++++---- main/xiaozhi-server/core/mcp/MCPClient.py | 24 +++++++++++++------- main/xiaozhi-server/core/utils/util.py | 5 ++++ 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/main/xiaozhi-server/core/handle/mcpHandle.py b/main/xiaozhi-server/core/handle/mcpHandle.py index cab8a96b..b5fd9372 100644 --- a/main/xiaozhi-server/core/handle/mcpHandle.py +++ b/main/xiaozhi-server/core/handle/mcpHandle.py @@ -1,7 +1,7 @@ import json import asyncio from concurrent.futures import Future -from core.utils.util import get_vision_url +from core.utils.util import get_vision_url, sanitize_tool_name from core.utils.auth import AuthToken TAG = __name__ @@ -11,7 +11,8 @@ class MCPClient: """MCPClient,用于管理MCP状态和工具""" def __init__(self): - self.tools = {} # Dictionary for O(1) lookup + self.tools = {} # sanitized_name -> tool_data + self.name_mapping = {} self.ready = False self.call_results = {} # To store Futures for tool call responses self.next_id = 1 @@ -30,7 +31,7 @@ class MCPClient: result = [] for tool_name, tool_data in self.tools.items(): function_def = { - "name": tool_data["name"], + "name": tool_name, "description": tool_data["description"], "parameters": { "type": tool_data["inputSchema"].get("type", "object"), @@ -53,7 +54,9 @@ class MCPClient: async def add_tool(self, tool_data: dict): async with self.lock: - self.tools[tool_data["name"]] = tool_data + sanitized_name = sanitize_tool_name(tool_data["name"]) + self.tools[sanitized_name] = tool_data + self.name_mapping[sanitized_name] = tool_data["name"] self._cached_available_tools = ( None # Invalidate the cache when a tool is added ) @@ -333,11 +336,12 @@ async def call_mcp_tool( raise ValueError(f"参数处理失败: {str(e)}") raise e + actual_name = mcp_client.name_mapping.get(tool_name, tool_name) payload = { "jsonrpc": "2.0", "id": tool_call_id, "method": "tools/call", - "params": {"name": tool_name, "arguments": arguments}, + "params": {"name": actual_name, "arguments": arguments}, } conn.logger.bind(tag=TAG).info( diff --git a/main/xiaozhi-server/core/mcp/MCPClient.py b/main/xiaozhi-server/core/mcp/MCPClient.py index b6b6335f..6af5f058 100644 --- a/main/xiaozhi-server/core/mcp/MCPClient.py +++ b/main/xiaozhi-server/core/mcp/MCPClient.py @@ -9,6 +9,7 @@ from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client from mcp.client.sse import sse_client from config.logger import setup_logging +from core.utils.util import sanitize_tool_name TAG = __name__ @@ -23,7 +24,9 @@ class MCPClient: self._shutdown_evt = asyncio.Event() self.session: Optional[ClientSession] = None - self.tools: List = [] + self.tools: List = [] # original tool objects + self.tools_dict: Dict[str, Any] = {} + self.name_mapping: Dict[str, str] = {} async def initialize(self): if self._worker_task: @@ -32,7 +35,7 @@ class MCPClient: await self._ready_evt.wait() self.logger.bind(tag=TAG).info( - f"Connected, tools = {[t.name for t in self.tools]}" + f"Connected, tools = {[name for name in self.name_mapping.values()]}" ) async def cleanup(self): @@ -48,27 +51,28 @@ class MCPClient: self._worker_task = None def has_tool(self, name: str) -> bool: - return any(t.name == name for t in self.tools) + return name in self.tools_dict def get_available_tools(self): return [ { "type": "function", "function": { - "name": t.name, - "description": t.description, - "parameters": t.inputSchema, + "name": name, + "description": tool.description, + "parameters": tool.inputSchema, }, } - for t in self.tools + for name, tool in self.tools_dict.items() ] async def call_tool(self, name: str, args: dict): if not self.session: raise RuntimeError("MCPClient not initialized") + real_name = self.name_mapping.get(name, name) loop = self._worker_task.get_loop() - coro = self.session.call_tool(name, args) + coro = self.session.call_tool(real_name, args) if loop is asyncio.get_running_loop(): return await coro @@ -123,6 +127,10 @@ class MCPClient: # 获取工具 self.tools = (await self.session.list_tools()).tools + for t in self.tools: + sanitized = sanitize_tool_name(t.name) + self.tools_dict[sanitized] = t + self.name_mapping[sanitized] = t.name self._ready_evt.set() diff --git a/main/xiaozhi-server/core/utils/util.py b/main/xiaozhi-server/core/utils/util.py index b492529c..cf52bfc2 100644 --- a/main/xiaozhi-server/core/utils/util.py +++ b/main/xiaozhi-server/core/utils/util.py @@ -976,3 +976,8 @@ def is_valid_image_file(file_data: bytes) -> bool: return True return False + + +def sanitize_tool_name(name: str) -> str: + """Sanitize tool names for OpenAI compatibility.""" + return re.sub(r"[^a-zA-Z0-9_-]", "_", name) From 182acc0787423e34011fd01e76cf6cbdc6d0c7b5 Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Sun, 8 Jun 2025 15:51:57 +0800 Subject: [PATCH 2/2] =?UTF-8?q?update:=E6=9B=BF=E6=8D=A2=E6=8F=8F=E8=BF=B0?= =?UTF-8?q?=E9=87=8C=E6=B6=89=E5=8F=8A=E5=88=B0=E7=9A=84=E5=8E=9F=E5=A7=8B?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E5=90=8D=E7=A7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/handle/mcpHandle.py | 23 ++++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/main/xiaozhi-server/core/handle/mcpHandle.py b/main/xiaozhi-server/core/handle/mcpHandle.py index b5fd9372..3f3216ac 100644 --- a/main/xiaozhi-server/core/handle/mcpHandle.py +++ b/main/xiaozhi-server/core/handle/mcpHandle.py @@ -136,9 +136,6 @@ async def handle_mcp_message(conn, mcp_client: MCPClient, payload: dict): conn.logger.bind(tag=TAG).info( f"客户端MCP服务器信息: name={name}, version={version}" ) - await send_mcp_tools_list_request( - conn - ) # After initialization, request tool list return elif msg_id == 2: # mcpToolsListID @@ -177,6 +174,20 @@ async def handle_mcp_message(conn, mcp_client: MCPClient, payload: dict): await mcp_client.add_tool(new_tool) conn.logger.bind(tag=TAG).debug(f"客户端工具 #{i+1}: {name}") + # 替换所有工具描述中的工具名称 + for tool_data in mcp_client.tools.values(): + if "description" in tool_data: + description = tool_data["description"] + # 遍历所有工具名称进行替换 + for ( + sanitized_name, + original_name, + ) in mcp_client.name_mapping.items(): + description = description.replace( + original_name, sanitized_name + ) + tool_data["description"] = description + next_cursor = result.get("nextCursor", "") if next_cursor: conn.logger.bind(tag=TAG).info( @@ -222,8 +233,6 @@ async def send_mcp_initialize_message(conn): "token": token, } - conn.logger.bind(tag=TAG).info(f"视觉服务信息: {vision}") - payload = { "jsonrpc": "2.0", "id": 1, # mcpInitializeID @@ -345,7 +354,7 @@ async def call_mcp_tool( } conn.logger.bind(tag=TAG).info( - f"发送客户端mcp工具调用请求: {tool_name},参数: {args}" + f"发送客户端mcp工具调用请求: {actual_name},参数: {args}" ) await send_mcp_message(conn, payload) @@ -353,7 +362,7 @@ async def call_mcp_tool( # Wait for response or timeout raw_result = await asyncio.wait_for(result_future, timeout=timeout) conn.logger.bind(tag=TAG).info( - f"客户端mcp工具调用 {tool_name} 成功,原始结果: {raw_result}" + f"客户端mcp工具调用 {actual_name} 成功,原始结果: {raw_result}" ) if isinstance(raw_result, dict):