From af1982c424b4cf920376cf237428fcc90036a026 Mon Sep 17 00:00:00 2001 From: shinjiyu Date: Mon, 26 Oct 2026 14:05:40 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20device=20MCP=20tools/call=20?= =?UTF-8?q?=E9=81=BF=E5=BC=80=E6=8F=A1=E6=89=8B=E5=8D=A0=E7=94=A8=E7=9A=84?= =?UTF-8?q?=20JSON-RPC=20id=201/2=20(#3462)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 握手阶段写死 id=1 (initialize) / id=2 (tools/list),但 MCPClient.next_id 从 1 起算,导致前两次 tools/call 复用同一连接上的 request id。合规设备端 会拒收并触发服务端 30s 超时(表现为前两次调用失败、第三次起恢复)。 将 next_id 初始值改为 3,与官方协议序列及 #3461 复现一致;同时去掉 mcp_handler 里重复的 MCPClient 定义,改为从 mcp_client 导入。 Fixes #3461 Co-authored-by: shinjiyu --- .../providers/tools/device_mcp/mcp_client.py | 6 +- .../providers/tools/device_mcp/mcp_handler.py | 89 +------------------ 2 files changed, 7 insertions(+), 88 deletions(-) diff --git a/main/xiaozhi-server/core/providers/tools/device_mcp/mcp_client.py b/main/xiaozhi-server/core/providers/tools/device_mcp/mcp_client.py index 75aa22b55..a6a8edc15 100644 --- a/main/xiaozhi-server/core/providers/tools/device_mcp/mcp_client.py +++ b/main/xiaozhi-server/core/providers/tools/device_mcp/mcp_client.py @@ -17,7 +17,11 @@ class MCPClient: self.name_mapping = {} self.ready = False self.call_results = {} # To store Futures for tool call responses - self.next_id = 1 + # Handshake hardcodes id=1 (initialize) and id=2 (tools/list) in mcp_handler. + # JSON-RPC 2.0 forbids reusing request ids on the same connection, and + # compliant device MCP servers reject tools/call with id 1/2 (see #3461). + # Official protocol sequence expects the first tools/call to use id=3. + self.next_id = 3 self.lock = asyncio.Lock() self._cached_available_tools = None # Cache for get_available_tools diff --git a/main/xiaozhi-server/core/providers/tools/device_mcp/mcp_handler.py b/main/xiaozhi-server/core/providers/tools/device_mcp/mcp_handler.py index 76994cabb..8d6a79687 100644 --- a/main/xiaozhi-server/core/providers/tools/device_mcp/mcp_handler.py +++ b/main/xiaozhi-server/core/providers/tools/device_mcp/mcp_handler.py @@ -3,10 +3,10 @@ import json import asyncio import re -from concurrent.futures import Future -from core.utils.util import get_vision_url, sanitize_tool_name +from core.utils.util import get_vision_url from core.utils.auth import AuthToken from config.logger import setup_logging +from .mcp_client import MCPClient from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -15,91 +15,6 @@ if TYPE_CHECKING: TAG = __name__ logger = setup_logging() - -class MCPClient: - """设备端MCP客户端,用于管理MCP状态和工具""" - - def __init__(self): - 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 - self.lock = asyncio.Lock() - self._cached_available_tools = None # Cache for get_available_tools - - def has_tool(self, name: str) -> bool: - return name in self.tools - - def get_available_tools(self) -> list: - # Check if the cache is valid - if self._cached_available_tools is not None: - return self._cached_available_tools - - # If cache is not valid, regenerate the list - result = [] - for tool_name, tool_data in self.tools.items(): - function_def = { - "name": tool_name, - "description": tool_data["description"], - "parameters": { - "type": tool_data["inputSchema"].get("type", "object"), - "properties": tool_data["inputSchema"].get("properties", {}), - "required": tool_data["inputSchema"].get("required", []), - }, - } - result.append({"type": "function", "function": function_def}) - - self._cached_available_tools = result # Store the generated list in cache - return result - - async def is_ready(self) -> bool: - async with self.lock: - return self.ready - - async def set_ready(self, status: bool): - async with self.lock: - self.ready = status - - async def add_tool(self, tool_data: dict): - async with self.lock: - 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 - ) - - async def get_next_id(self) -> int: - async with self.lock: - current_id = self.next_id - self.next_id += 1 - return current_id - - async def register_call_result_future(self, id: int, future: Future): - async with self.lock: - self.call_results[id] = future - - async def resolve_call_result(self, id: int, result: any): - async with self.lock: - if id in self.call_results: - future = self.call_results.pop(id) - if not future.done(): - future.set_result(result) - - async def reject_call_result(self, id: int, exception: Exception): - async with self.lock: - if id in self.call_results: - future = self.call_results.pop(id) - if not future.done(): - future.set_exception(exception) - - async def cleanup_call_result(self, id: int): - async with self.lock: - if id in self.call_results: - self.call_results.pop(id) - - async def send_mcp_message(conn: "ConnectionHandler", payload: dict): """Helper to send MCP messages, encapsulating common logic.""" if not conn.features.get("mcp"): From 05f0da063934256cb8cdb9ad6f64ae864f67937d Mon Sep 17 00:00:00 2001 From: Sakura-RanChen <1908198662@qq.com> Date: Mon, 26 Oct 2026 14:40:31 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E8=A1=A5=E5=85=85=E6=8E=A5=E5=85=A5?= =?UTF-8?q?=E7=82=B9=E5=B7=A5=E5=85=B7id=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/providers/tools/mcp_endpoint/mcp_endpoint_client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/main/xiaozhi-server/core/providers/tools/mcp_endpoint/mcp_endpoint_client.py b/main/xiaozhi-server/core/providers/tools/mcp_endpoint/mcp_endpoint_client.py index 4e9e5303f..b20ee6221 100644 --- a/main/xiaozhi-server/core/providers/tools/mcp_endpoint/mcp_endpoint_client.py +++ b/main/xiaozhi-server/core/providers/tools/mcp_endpoint/mcp_endpoint_client.py @@ -18,7 +18,9 @@ class MCPEndpointClient: self.name_mapping = {} self.ready = False self.call_results = {} # To store Futures for tool call responses - self.next_id = 1 + # Handshake hardcodes id=1 (initialize) and id=2 (tools/list) in + # mcp_endpoint_handler; tools/call must start at 3 to avoid id reuse. + self.next_id = 3 self.lock = asyncio.Lock() self._cached_available_tools = None # Cache for get_available_tools self.websocket = None # WebSocket连接