|
@@ -1,13 +1,17 @@
|
|
|
-"""设备端MCP客户端支持模块"""
|
|
|
|
|
|
|
+"""Device-side MCP client support."""
|
|
|
|
|
|
|
|
-import json
|
|
|
|
|
import asyncio
|
|
import asyncio
|
|
|
|
|
+import json
|
|
|
import re
|
|
import re
|
|
|
|
|
+import uuid
|
|
|
from concurrent.futures import Future
|
|
from concurrent.futures import Future
|
|
|
-from core.utils.util import get_vision_url, sanitize_tool_name
|
|
|
|
|
-from core.utils.auth import AuthToken
|
|
|
|
|
|
|
+from typing import TYPE_CHECKING, Any
|
|
|
|
|
+
|
|
|
from config.logger import setup_logging
|
|
from config.logger import setup_logging
|
|
|
-from typing import TYPE_CHECKING
|
|
|
|
|
|
|
+from core.providers.tts.dto.dto import ContentType, SentenceType, TTSMessageDTO
|
|
|
|
|
+from core.utils.auth import AuthToken
|
|
|
|
|
+from core.utils.dialogue import Message
|
|
|
|
|
+from core.utils.util import get_vision_url, sanitize_tool_name
|
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
if TYPE_CHECKING:
|
|
|
from core.connection import ConnectionHandler
|
|
from core.connection import ConnectionHandler
|
|
@@ -17,26 +21,24 @@ logger = setup_logging()
|
|
|
|
|
|
|
|
|
|
|
|
|
class MCPClient:
|
|
class MCPClient:
|
|
|
- """设备端MCP客户端,用于管理MCP状态和工具"""
|
|
|
|
|
|
|
+ """Tracks device MCP tools and pending call results."""
|
|
|
|
|
|
|
|
def __init__(self):
|
|
def __init__(self):
|
|
|
- self.tools = {} # sanitized_name -> tool_data
|
|
|
|
|
- self.name_mapping = {}
|
|
|
|
|
|
|
+ self.tools: dict[str, dict[str, Any]] = {}
|
|
|
|
|
+ self.name_mapping: dict[str, str] = {}
|
|
|
self.ready = False
|
|
self.ready = False
|
|
|
- self.call_results = {} # To store Futures for tool call responses
|
|
|
|
|
|
|
+ self.call_results: dict[int, Future] = {}
|
|
|
self.next_id = 1
|
|
self.next_id = 1
|
|
|
self.lock = asyncio.Lock()
|
|
self.lock = asyncio.Lock()
|
|
|
- self._cached_available_tools = None # Cache for get_available_tools
|
|
|
|
|
|
|
+ self._cached_available_tools = None
|
|
|
|
|
|
|
|
def has_tool(self, name: str) -> bool:
|
|
def has_tool(self, name: str) -> bool:
|
|
|
return name in self.tools
|
|
return name in self.tools
|
|
|
|
|
|
|
|
def get_available_tools(self) -> list:
|
|
def get_available_tools(self) -> list:
|
|
|
- # Check if the cache is valid
|
|
|
|
|
if self._cached_available_tools is not None:
|
|
if self._cached_available_tools is not None:
|
|
|
return self._cached_available_tools
|
|
return self._cached_available_tools
|
|
|
|
|
|
|
|
- # If cache is not valid, regenerate the list
|
|
|
|
|
result = []
|
|
result = []
|
|
|
for tool_name, tool_data in self.tools.items():
|
|
for tool_name, tool_data in self.tools.items():
|
|
|
function_def = {
|
|
function_def = {
|
|
@@ -50,7 +52,7 @@ class MCPClient:
|
|
|
}
|
|
}
|
|
|
result.append({"type": "function", "function": function_def})
|
|
result.append({"type": "function", "function": function_def})
|
|
|
|
|
|
|
|
- self._cached_available_tools = result # Store the generated list in cache
|
|
|
|
|
|
|
+ self._cached_available_tools = result
|
|
|
return result
|
|
return result
|
|
|
|
|
|
|
|
async def is_ready(self) -> bool:
|
|
async def is_ready(self) -> bool:
|
|
@@ -66,9 +68,7 @@ class MCPClient:
|
|
|
sanitized_name = sanitize_tool_name(tool_data["name"])
|
|
sanitized_name = sanitize_tool_name(tool_data["name"])
|
|
|
self.tools[sanitized_name] = tool_data
|
|
self.tools[sanitized_name] = tool_data
|
|
|
self.name_mapping[sanitized_name] = tool_data["name"]
|
|
self.name_mapping[sanitized_name] = tool_data["name"]
|
|
|
- self._cached_available_tools = (
|
|
|
|
|
- None # Invalidate the cache when a tool is added
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ self._cached_available_tools = None
|
|
|
|
|
|
|
|
async def get_next_id(self) -> int:
|
|
async def get_next_id(self) -> int:
|
|
|
async with self.lock:
|
|
async with self.lock:
|
|
@@ -76,189 +76,211 @@ class MCPClient:
|
|
|
self.next_id += 1
|
|
self.next_id += 1
|
|
|
return current_id
|
|
return current_id
|
|
|
|
|
|
|
|
- async def register_call_result_future(self, id: int, future: Future):
|
|
|
|
|
|
|
+ async def register_call_result_future(self, call_id: int, future: Future):
|
|
|
async with self.lock:
|
|
async with self.lock:
|
|
|
- self.call_results[id] = future
|
|
|
|
|
|
|
+ self.call_results[call_id] = future
|
|
|
|
|
|
|
|
- async def resolve_call_result(self, id: int, result: any):
|
|
|
|
|
|
|
+ async def resolve_call_result(self, call_id: int, result: Any):
|
|
|
async with self.lock:
|
|
async with self.lock:
|
|
|
- if id in self.call_results:
|
|
|
|
|
- future = self.call_results.pop(id)
|
|
|
|
|
|
|
+ if call_id in self.call_results:
|
|
|
|
|
+ future = self.call_results.pop(call_id)
|
|
|
if not future.done():
|
|
if not future.done():
|
|
|
future.set_result(result)
|
|
future.set_result(result)
|
|
|
|
|
|
|
|
- async def reject_call_result(self, id: int, exception: Exception):
|
|
|
|
|
|
|
+ async def reject_call_result(self, call_id: int, exception: Exception):
|
|
|
async with self.lock:
|
|
async with self.lock:
|
|
|
- if id in self.call_results:
|
|
|
|
|
- future = self.call_results.pop(id)
|
|
|
|
|
|
|
+ if call_id in self.call_results:
|
|
|
|
|
+ future = self.call_results.pop(call_id)
|
|
|
if not future.done():
|
|
if not future.done():
|
|
|
future.set_exception(exception)
|
|
future.set_exception(exception)
|
|
|
|
|
|
|
|
- async def cleanup_call_result(self, id: int):
|
|
|
|
|
|
|
+ async def cleanup_call_result(self, call_id: int):
|
|
|
async with self.lock:
|
|
async with self.lock:
|
|
|
- if id in self.call_results:
|
|
|
|
|
- self.call_results.pop(id)
|
|
|
|
|
|
|
+ if call_id in self.call_results:
|
|
|
|
|
+ self.call_results.pop(call_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
async def send_mcp_message(conn: "ConnectionHandler", payload: dict):
|
|
async def send_mcp_message(conn: "ConnectionHandler", payload: dict):
|
|
|
- """Helper to send MCP messages, encapsulating common logic."""
|
|
|
|
|
- if not conn.features.get("mcp"):
|
|
|
|
|
- logger.bind(tag=TAG).warning("客户端不支持MCP,无法发送MCP消息")
|
|
|
|
|
|
|
+ """Send an MCP message to the device if the connection supports MCP."""
|
|
|
|
|
+ if not conn.features or not conn.features.get("mcp"):
|
|
|
|
|
+ logger.bind(tag=TAG).warning("Client does not support MCP, skipping message")
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
message = json.dumps({"type": "mcp", "payload": payload})
|
|
message = json.dumps({"type": "mcp", "payload": payload})
|
|
|
-
|
|
|
|
|
try:
|
|
try:
|
|
|
await conn.websocket.send(message)
|
|
await conn.websocket.send(message)
|
|
|
- logger.bind(tag=TAG).debug(f"成功发送MCP消息: {message}")
|
|
|
|
|
|
|
+ logger.bind(tag=TAG).debug(f"Sent MCP message: {message}")
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
- logger.bind(tag=TAG).error(f"发送MCP消息失败: {e}")
|
|
|
|
|
|
|
+ logger.bind(tag=TAG).error(f"Failed to send MCP message: {e}")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def _enqueue_device_tts(
|
|
|
|
|
+ conn: "ConnectionHandler", text: str, interrupt: bool = True
|
|
|
|
|
+):
|
|
|
|
|
+ if interrupt:
|
|
|
|
|
+ conn.client_abort = True
|
|
|
|
|
+ conn.clear_queues()
|
|
|
|
|
+ conn.clearSpeakStatus()
|
|
|
|
|
+
|
|
|
|
|
+ conn.client_abort = False
|
|
|
|
|
+ conn.sentence_id = uuid.uuid4().hex
|
|
|
|
|
+ conn.tts.store_tts_text(conn.sentence_id, text)
|
|
|
|
|
+ conn.tts.tts_text_queue.put(
|
|
|
|
|
+ TTSMessageDTO(
|
|
|
|
|
+ sentence_id=conn.sentence_id,
|
|
|
|
|
+ sentence_type=SentenceType.FIRST,
|
|
|
|
|
+ content_type=ContentType.ACTION,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ conn.tts.tts_one_sentence(conn, ContentType.TEXT, content_detail=text)
|
|
|
|
|
+ conn.tts.tts_text_queue.put(
|
|
|
|
|
+ TTSMessageDTO(
|
|
|
|
|
+ sentence_id=conn.sentence_id,
|
|
|
|
|
+ sentence_type=SentenceType.LAST,
|
|
|
|
|
+ content_type=ContentType.ACTION,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ conn.dialogue.put(Message(role="assistant", content=text, is_temporary=True))
|
|
|
|
|
|
|
|
|
|
|
|
|
async def handle_mcp_message(
|
|
async def handle_mcp_message(
|
|
|
conn: "ConnectionHandler", mcp_client: MCPClient, payload: dict
|
|
conn: "ConnectionHandler", mcp_client: MCPClient, payload: dict
|
|
|
):
|
|
):
|
|
|
- """处理MCP消息,包括初始化、工具列表和工具调用响应等"""
|
|
|
|
|
- logger.bind(tag=TAG).debug(f"处理MCP消息: {str(payload)[:100]}")
|
|
|
|
|
|
|
+ logger.bind(tag=TAG).debug(f"Handling MCP payload: {str(payload)[:200]}")
|
|
|
|
|
|
|
|
if not isinstance(payload, dict):
|
|
if not isinstance(payload, dict):
|
|
|
- logger.bind(tag=TAG).error("MCP消息缺少payload字段或格式错误")
|
|
|
|
|
|
|
+ logger.bind(tag=TAG).error("Invalid MCP payload format")
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
- # Handle result
|
|
|
|
|
if "result" in payload:
|
|
if "result" in payload:
|
|
|
result = payload["result"]
|
|
result = payload["result"]
|
|
|
msg_id = int(payload.get("id", 0))
|
|
msg_id = int(payload.get("id", 0))
|
|
|
|
|
|
|
|
- # Check for tool call response first
|
|
|
|
|
if msg_id in mcp_client.call_results:
|
|
if msg_id in mcp_client.call_results:
|
|
|
logger.bind(tag=TAG).debug(
|
|
logger.bind(tag=TAG).debug(
|
|
|
- f"收到工具调用响应,ID: {msg_id}, 结果: {result}"
|
|
|
|
|
|
|
+ f"Received MCP tool call result id={msg_id}: {result}"
|
|
|
)
|
|
)
|
|
|
await mcp_client.resolve_call_result(msg_id, result)
|
|
await mcp_client.resolve_call_result(msg_id, result)
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
- if msg_id == 1: # mcpInitializeID
|
|
|
|
|
- logger.bind(tag=TAG).debug("收到MCP初始化响应")
|
|
|
|
|
|
|
+ if msg_id == 1:
|
|
|
|
|
+ logger.bind(tag=TAG).debug("Received MCP initialize response")
|
|
|
server_info = result.get("serverInfo")
|
|
server_info = result.get("serverInfo")
|
|
|
if isinstance(server_info, dict):
|
|
if isinstance(server_info, dict):
|
|
|
- name = server_info.get("name")
|
|
|
|
|
- version = server_info.get("version")
|
|
|
|
|
logger.bind(tag=TAG).debug(
|
|
logger.bind(tag=TAG).debug(
|
|
|
- f"客户端MCP服务器信息: name={name}, version={version}"
|
|
|
|
|
|
|
+ "Device MCP server info: name=%s version=%s",
|
|
|
|
|
+ server_info.get("name"),
|
|
|
|
|
+ server_info.get("version"),
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
await asyncio.sleep(1)
|
|
await asyncio.sleep(1)
|
|
|
- logger.bind(tag=TAG).debug("初始化完成,开始请求MCP工具列表")
|
|
|
|
|
await send_mcp_tools_list_request(conn)
|
|
await send_mcp_tools_list_request(conn)
|
|
|
-
|
|
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
- elif msg_id == 2: # mcpToolsListID
|
|
|
|
|
- logger.bind(tag=TAG).debug("收到MCP工具列表响应")
|
|
|
|
|
- if isinstance(result, dict) and "tools" in result:
|
|
|
|
|
- tools_data = result["tools"]
|
|
|
|
|
- if not isinstance(tools_data, list):
|
|
|
|
|
- logger.bind(tag=TAG).error("工具列表格式错误")
|
|
|
|
|
- return
|
|
|
|
|
|
|
+ if msg_id == 2:
|
|
|
|
|
+ logger.bind(tag=TAG).debug("Received MCP tools list response")
|
|
|
|
|
+ if not isinstance(result, dict) or "tools" not in result:
|
|
|
|
|
+ return
|
|
|
|
|
|
|
|
- logger.bind(tag=TAG).info(
|
|
|
|
|
- f"客户端设备支持的工具数量: {len(tools_data)}"
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ tools_data = result["tools"]
|
|
|
|
|
+ if not isinstance(tools_data, list):
|
|
|
|
|
+ logger.bind(tag=TAG).error("Invalid MCP tools list format")
|
|
|
|
|
+ return
|
|
|
|
|
|
|
|
- for i, tool in enumerate(tools_data):
|
|
|
|
|
- if not isinstance(tool, dict):
|
|
|
|
|
- continue
|
|
|
|
|
-
|
|
|
|
|
- name = tool.get("name", "")
|
|
|
|
|
- description = tool.get("description", "")
|
|
|
|
|
- input_schema = {"type": "object", "properties": {}, "required": []}
|
|
|
|
|
-
|
|
|
|
|
- if "inputSchema" in tool and isinstance(tool["inputSchema"], dict):
|
|
|
|
|
- schema = tool["inputSchema"]
|
|
|
|
|
- input_schema["type"] = schema.get("type", "object")
|
|
|
|
|
- input_schema["properties"] = schema.get("properties", {})
|
|
|
|
|
- input_schema["required"] = [
|
|
|
|
|
- s for s in schema.get("required", []) if isinstance(s, str)
|
|
|
|
|
- ]
|
|
|
|
|
-
|
|
|
|
|
- new_tool = {
|
|
|
|
|
- "name": name,
|
|
|
|
|
- "description": description,
|
|
|
|
|
- "inputSchema": input_schema,
|
|
|
|
|
- }
|
|
|
|
|
- await mcp_client.add_tool(new_tool)
|
|
|
|
|
- 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:
|
|
|
|
|
- logger.bind(tag=TAG).debug(f"有更多工具,nextCursor: {next_cursor}")
|
|
|
|
|
- await send_mcp_tools_list_continue_request(conn, next_cursor)
|
|
|
|
|
- else:
|
|
|
|
|
- await mcp_client.set_ready(True)
|
|
|
|
|
- logger.bind(tag=TAG).debug("所有工具已获取,MCP客户端准备就绪")
|
|
|
|
|
-
|
|
|
|
|
- # 刷新工具缓存,确保MCP工具被包含在函数列表中
|
|
|
|
|
- if hasattr(conn, "func_handler") and conn.func_handler:
|
|
|
|
|
- conn.func_handler.tool_manager.refresh_tools()
|
|
|
|
|
- conn.func_handler.current_support_functions()
|
|
|
|
|
|
|
+ logger.bind(tag=TAG).info(
|
|
|
|
|
+ f"Device reported {len(tools_data)} MCP tools"
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ for tool in tools_data:
|
|
|
|
|
+ if not isinstance(tool, dict):
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ schema = tool.get("inputSchema", {})
|
|
|
|
|
+ if not isinstance(schema, dict):
|
|
|
|
|
+ schema = {}
|
|
|
|
|
+
|
|
|
|
|
+ new_tool = {
|
|
|
|
|
+ "name": tool.get("name", ""),
|
|
|
|
|
+ "description": tool.get("description", ""),
|
|
|
|
|
+ "inputSchema": {
|
|
|
|
|
+ "type": schema.get("type", "object"),
|
|
|
|
|
+ "properties": schema.get("properties", {}),
|
|
|
|
|
+ "required": [
|
|
|
|
|
+ item
|
|
|
|
|
+ for item in schema.get("required", [])
|
|
|
|
|
+ if isinstance(item, str)
|
|
|
|
|
+ ],
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+ await mcp_client.add_tool(new_tool)
|
|
|
|
|
+
|
|
|
|
|
+ for tool_data in mcp_client.tools.values():
|
|
|
|
|
+ description = tool_data.get("description")
|
|
|
|
|
+ if not isinstance(description, str):
|
|
|
|
|
+ continue
|
|
|
|
|
+ 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:
|
|
|
|
|
+ await send_mcp_tools_list_continue_request(conn, next_cursor)
|
|
|
|
|
+ else:
|
|
|
|
|
+ await mcp_client.set_ready(True)
|
|
|
|
|
+ if hasattr(conn, "func_handler") and conn.func_handler:
|
|
|
|
|
+ conn.func_handler.tool_manager.refresh_tools()
|
|
|
|
|
+ conn.func_handler.current_support_functions()
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
- # Handle method calls (requests from the client)
|
|
|
|
|
elif "method" in payload:
|
|
elif "method" in payload:
|
|
|
method = payload["method"]
|
|
method = payload["method"]
|
|
|
- logger.bind(tag=TAG).info(f"收到MCP客户端请求: {method}")
|
|
|
|
|
|
|
+ logger.bind(tag=TAG).info(f"Received MCP request from device: {method}")
|
|
|
|
|
+
|
|
|
|
|
+ if method == "nav.alert.speak":
|
|
|
|
|
+ params = payload.get("params", {})
|
|
|
|
|
+ if not isinstance(params, dict):
|
|
|
|
|
+ logger.bind(tag=TAG).warning("nav.alert.speak params are invalid")
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ text = params.get("text", "")
|
|
|
|
|
+ interrupt = bool(params.get("interrupt", True))
|
|
|
|
|
+ if not isinstance(text, str) or not text.strip():
|
|
|
|
|
+ logger.bind(tag=TAG).warning("nav.alert.speak missing text")
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ await _enqueue_device_tts(conn, text.strip(), interrupt)
|
|
|
|
|
+ return
|
|
|
|
|
|
|
|
elif "error" in payload:
|
|
elif "error" in payload:
|
|
|
error_data = payload["error"]
|
|
error_data = payload["error"]
|
|
|
- error_msg = error_data.get("message", "未知错误")
|
|
|
|
|
- logger.bind(tag=TAG).error(f"收到MCP错误响应: {error_msg}")
|
|
|
|
|
|
|
+ error_msg = error_data.get("message", "Unknown MCP error")
|
|
|
|
|
+ logger.bind(tag=TAG).error(f"Received MCP error response: {error_msg}")
|
|
|
|
|
|
|
|
msg_id = int(payload.get("id", 0))
|
|
msg_id = int(payload.get("id", 0))
|
|
|
if msg_id in mcp_client.call_results:
|
|
if msg_id in mcp_client.call_results:
|
|
|
await mcp_client.reject_call_result(
|
|
await mcp_client.reject_call_result(
|
|
|
- msg_id, Exception(f"MCP错误: {error_msg}")
|
|
|
|
|
|
|
+ msg_id, Exception(f"MCP error: {error_msg}")
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
async def send_mcp_initialize_message(conn: "ConnectionHandler"):
|
|
async def send_mcp_initialize_message(conn: "ConnectionHandler"):
|
|
|
- """发送MCP初始化消息"""
|
|
|
|
|
-
|
|
|
|
|
vision_url = get_vision_url(conn.config)
|
|
vision_url = get_vision_url(conn.config)
|
|
|
-
|
|
|
|
|
- # 密钥生成token
|
|
|
|
|
auth = AuthToken(conn.config["server"]["auth_key"])
|
|
auth = AuthToken(conn.config["server"]["auth_key"])
|
|
|
token = auth.generate_token(conn.headers.get("device-id"))
|
|
token = auth.generate_token(conn.headers.get("device-id"))
|
|
|
|
|
|
|
|
- vision = {
|
|
|
|
|
- "url": vision_url,
|
|
|
|
|
- "token": token,
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
payload = {
|
|
payload = {
|
|
|
"jsonrpc": "2.0",
|
|
"jsonrpc": "2.0",
|
|
|
- "id": 1, # mcpInitializeID
|
|
|
|
|
|
|
+ "id": 1,
|
|
|
"method": "initialize",
|
|
"method": "initialize",
|
|
|
"params": {
|
|
"params": {
|
|
|
"protocolVersion": "2024-11-05",
|
|
"protocolVersion": "2024-11-05",
|
|
|
"capabilities": {
|
|
"capabilities": {
|
|
|
"roots": {"listChanged": True},
|
|
"roots": {"listChanged": True},
|
|
|
"sampling": {},
|
|
"sampling": {},
|
|
|
- "vision": vision,
|
|
|
|
|
|
|
+ "vision": {
|
|
|
|
|
+ "url": vision_url,
|
|
|
|
|
+ "token": token,
|
|
|
|
|
+ },
|
|
|
},
|
|
},
|
|
|
"clientInfo": {
|
|
"clientInfo": {
|
|
|
"name": "XiaozhiClient",
|
|
"name": "XiaozhiClient",
|
|
@@ -266,30 +288,30 @@ async def send_mcp_initialize_message(conn: "ConnectionHandler"):
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
}
|
|
}
|
|
|
- logger.bind(tag=TAG).debug("发送MCP初始化消息")
|
|
|
|
|
|
|
+ logger.bind(tag=TAG).debug("Sending MCP initialize message")
|
|
|
await send_mcp_message(conn, payload)
|
|
await send_mcp_message(conn, payload)
|
|
|
|
|
|
|
|
|
|
|
|
|
async def send_mcp_tools_list_request(conn: "ConnectionHandler"):
|
|
async def send_mcp_tools_list_request(conn: "ConnectionHandler"):
|
|
|
- """发送MCP工具列表请求"""
|
|
|
|
|
payload = {
|
|
payload = {
|
|
|
"jsonrpc": "2.0",
|
|
"jsonrpc": "2.0",
|
|
|
- "id": 2, # mcpToolsListID
|
|
|
|
|
|
|
+ "id": 2,
|
|
|
"method": "tools/list",
|
|
"method": "tools/list",
|
|
|
}
|
|
}
|
|
|
- logger.bind(tag=TAG).debug("发送MCP工具列表请求")
|
|
|
|
|
|
|
+ logger.bind(tag=TAG).debug("Sending MCP tools list request")
|
|
|
await send_mcp_message(conn, payload)
|
|
await send_mcp_message(conn, payload)
|
|
|
|
|
|
|
|
|
|
|
|
|
-async def send_mcp_tools_list_continue_request(conn: "ConnectionHandler", cursor: str):
|
|
|
|
|
- """发送带有cursor的MCP工具列表请求"""
|
|
|
|
|
|
|
+async def send_mcp_tools_list_continue_request(
|
|
|
|
|
+ conn: "ConnectionHandler", cursor: str
|
|
|
|
|
+):
|
|
|
payload = {
|
|
payload = {
|
|
|
"jsonrpc": "2.0",
|
|
"jsonrpc": "2.0",
|
|
|
- "id": 2, # mcpToolsListID (same ID for continuation)
|
|
|
|
|
|
|
+ "id": 2,
|
|
|
"method": "tools/list",
|
|
"method": "tools/list",
|
|
|
"params": {"cursor": cursor},
|
|
"params": {"cursor": cursor},
|
|
|
}
|
|
}
|
|
|
- logger.bind(tag=TAG).info(f"发送带cursor的MCP工具列表请求: {cursor}")
|
|
|
|
|
|
|
+ logger.bind(tag=TAG).info(f"Sending MCP tools list continuation: {cursor}")
|
|
|
await send_mcp_message(conn, payload)
|
|
await send_mcp_message(conn, payload)
|
|
|
|
|
|
|
|
|
|
|
|
@@ -300,36 +322,27 @@ async def call_mcp_tool(
|
|
|
args: str = "{}",
|
|
args: str = "{}",
|
|
|
timeout: int = 30,
|
|
timeout: int = 30,
|
|
|
):
|
|
):
|
|
|
- """
|
|
|
|
|
- 调用指定的工具,并等待响应
|
|
|
|
|
- """
|
|
|
|
|
if not await mcp_client.is_ready():
|
|
if not await mcp_client.is_ready():
|
|
|
- raise RuntimeError("MCP客户端尚未准备就绪")
|
|
|
|
|
|
|
+ raise RuntimeError("MCP client is not ready")
|
|
|
|
|
|
|
|
if not mcp_client.has_tool(tool_name):
|
|
if not mcp_client.has_tool(tool_name):
|
|
|
- raise ValueError(f"工具 {tool_name} 不存在")
|
|
|
|
|
|
|
+ raise ValueError(f"Tool {tool_name} does not exist")
|
|
|
|
|
|
|
|
tool_call_id = await mcp_client.get_next_id()
|
|
tool_call_id = await mcp_client.get_next_id()
|
|
|
result_future = asyncio.Future()
|
|
result_future = asyncio.Future()
|
|
|
await mcp_client.register_call_result_future(tool_call_id, result_future)
|
|
await mcp_client.register_call_result_future(tool_call_id, result_future)
|
|
|
|
|
|
|
|
- # 处理参数
|
|
|
|
|
try:
|
|
try:
|
|
|
if isinstance(args, str):
|
|
if isinstance(args, str):
|
|
|
- # 确保字符串是有效的JSON
|
|
|
|
|
if not args.strip():
|
|
if not args.strip():
|
|
|
arguments = {}
|
|
arguments = {}
|
|
|
else:
|
|
else:
|
|
|
try:
|
|
try:
|
|
|
- # 尝试直接解析
|
|
|
|
|
arguments = json.loads(args)
|
|
arguments = json.loads(args)
|
|
|
except json.JSONDecodeError:
|
|
except json.JSONDecodeError:
|
|
|
- # 如果解析失败,尝试合并多个JSON对象
|
|
|
|
|
try:
|
|
try:
|
|
|
- # 使用正则表达式匹配所有JSON对象
|
|
|
|
|
json_objects = re.findall(r"\{[^{}]*\}", args)
|
|
json_objects = re.findall(r"\{[^{}]*\}", args)
|
|
|
if len(json_objects) > 1:
|
|
if len(json_objects) > 1:
|
|
|
- # 合并所有JSON对象
|
|
|
|
|
merged_dict = {}
|
|
merged_dict = {}
|
|
|
for json_str in json_objects:
|
|
for json_str in json_objects:
|
|
|
try:
|
|
try:
|
|
@@ -341,27 +354,32 @@ async def call_mcp_tool(
|
|
|
if merged_dict:
|
|
if merged_dict:
|
|
|
arguments = merged_dict
|
|
arguments = merged_dict
|
|
|
else:
|
|
else:
|
|
|
- raise ValueError(f"无法解析任何有效的JSON对象: {args}")
|
|
|
|
|
|
|
+ raise ValueError(
|
|
|
|
|
+ f"Unable to parse any valid JSON object: {args}"
|
|
|
|
|
+ )
|
|
|
else:
|
|
else:
|
|
|
- raise ValueError(f"参数JSON解析失败: {args}")
|
|
|
|
|
|
|
+ raise ValueError(f"Failed to parse JSON arguments: {args}")
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
logger.bind(tag=TAG).error(
|
|
logger.bind(tag=TAG).error(
|
|
|
- f"参数JSON解析失败: {str(e)}, 原始参数: {args}"
|
|
|
|
|
|
|
+ f"Failed to parse tool arguments: {e}, raw={args}"
|
|
|
)
|
|
)
|
|
|
- raise ValueError(f"参数JSON解析失败: {str(e)}")
|
|
|
|
|
|
|
+ raise ValueError(f"Failed to parse tool arguments: {e}")
|
|
|
elif isinstance(args, dict):
|
|
elif isinstance(args, dict):
|
|
|
arguments = args
|
|
arguments = args
|
|
|
else:
|
|
else:
|
|
|
- raise ValueError(f"参数类型错误,期望字符串或字典,实际类型: {type(args)}")
|
|
|
|
|
|
|
+ raise ValueError(
|
|
|
|
|
+ f"Invalid arguments type: expected str or dict, got {type(args)}"
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
- # 确保参数是字典类型
|
|
|
|
|
if not isinstance(arguments, dict):
|
|
if not isinstance(arguments, dict):
|
|
|
- raise ValueError(f"参数必须是字典类型,实际类型: {type(arguments)}")
|
|
|
|
|
|
|
+ raise ValueError(
|
|
|
|
|
+ f"Arguments must be a dict, got {type(arguments)}"
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
if not isinstance(e, ValueError):
|
|
if not isinstance(e, ValueError):
|
|
|
- raise ValueError(f"参数处理失败: {str(e)}")
|
|
|
|
|
- raise e
|
|
|
|
|
|
|
+ raise ValueError(f"Failed to process tool arguments: {e}")
|
|
|
|
|
+ raise
|
|
|
|
|
|
|
|
actual_name = mcp_client.name_mapping.get(tool_name, tool_name)
|
|
actual_name = mcp_client.name_mapping.get(tool_name, tool_name)
|
|
|
payload = {
|
|
payload = {
|
|
@@ -371,33 +389,34 @@ async def call_mcp_tool(
|
|
|
"params": {"name": actual_name, "arguments": arguments},
|
|
"params": {"name": actual_name, "arguments": arguments},
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- logger.bind(tag=TAG).info(f"发送客户端mcp工具调用请求: {actual_name},参数: {args}")
|
|
|
|
|
|
|
+ logger.bind(tag=TAG).info(
|
|
|
|
|
+ f"Sending device MCP tool call: {actual_name}, args={arguments}"
|
|
|
|
|
+ )
|
|
|
await send_mcp_message(conn, payload)
|
|
await send_mcp_message(conn, payload)
|
|
|
|
|
|
|
|
try:
|
|
try:
|
|
|
- # Wait for response or timeout
|
|
|
|
|
raw_result = await asyncio.wait_for(result_future, timeout=timeout)
|
|
raw_result = await asyncio.wait_for(result_future, timeout=timeout)
|
|
|
logger.bind(tag=TAG).info(
|
|
logger.bind(tag=TAG).info(
|
|
|
- f"客户端mcp工具调用 {actual_name} 成功,原始结果: {raw_result}"
|
|
|
|
|
|
|
+ f"Device MCP tool call succeeded: {actual_name}, result={raw_result}"
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
if isinstance(raw_result, dict):
|
|
if isinstance(raw_result, dict):
|
|
|
if raw_result.get("isError") is True:
|
|
if raw_result.get("isError") is True:
|
|
|
error_msg = raw_result.get(
|
|
error_msg = raw_result.get(
|
|
|
- "error", "工具调用返回错误,但未提供具体错误信息"
|
|
|
|
|
|
|
+ "error",
|
|
|
|
|
+ "Tool call returned an error without details",
|
|
|
)
|
|
)
|
|
|
- raise RuntimeError(f"工具调用错误: {error_msg}")
|
|
|
|
|
|
|
+ raise RuntimeError(f"Tool call error: {error_msg}")
|
|
|
|
|
|
|
|
content = raw_result.get("content")
|
|
content = raw_result.get("content")
|
|
|
if isinstance(content, list) and len(content) > 0:
|
|
if isinstance(content, list) and len(content) > 0:
|
|
|
if isinstance(content[0], dict) and "text" in content[0]:
|
|
if isinstance(content[0], dict) and "text" in content[0]:
|
|
|
- # 直接返回文本内容,不进行JSON解析
|
|
|
|
|
return content[0]["text"]
|
|
return content[0]["text"]
|
|
|
- # 如果结果不是预期的格式,将其转换为字符串
|
|
|
|
|
|
|
+
|
|
|
return str(raw_result)
|
|
return str(raw_result)
|
|
|
except asyncio.TimeoutError:
|
|
except asyncio.TimeoutError:
|
|
|
await mcp_client.cleanup_call_result(tool_call_id)
|
|
await mcp_client.cleanup_call_result(tool_call_id)
|
|
|
- raise TimeoutError("工具调用请求超时")
|
|
|
|
|
- except Exception as e:
|
|
|
|
|
|
|
+ raise TimeoutError("Tool call request timed out")
|
|
|
|
|
+ except Exception:
|
|
|
await mcp_client.cleanup_call_result(tool_call_id)
|
|
await mcp_client.cleanup_call_result(tool_call_id)
|
|
|
- raise e
|
|
|
|
|
|
|
+ raise
|