mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-29 18:23:59 +08:00
resolve merge conflict
This commit is contained in:
@@ -116,14 +116,14 @@ async def send_mcp_message(conn, payload: dict, transport=None):
|
||||
await conn.transport.send(message)
|
||||
else:
|
||||
raise AttributeError("无法找到可用的传输层接口")
|
||||
logger.bind(tag=TAG).info(f"成功发送MCP消息: {message}")
|
||||
logger.bind(tag=TAG).debug(f"成功发送MCP消息: {message}")
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"发送MCP消息失败: {e}")
|
||||
|
||||
|
||||
async def handle_mcp_message(conn, mcp_client: MCPClient, payload: dict, transport=None):
|
||||
"""处理MCP消息,包括初始化、工具列表和工具调用响应等"""
|
||||
logger.bind(tag=TAG).info(f"处理MCP消息: {str(payload)[:100]}")
|
||||
logger.bind(tag=TAG).debug(f"处理MCP消息: {str(payload)[:100]}")
|
||||
|
||||
if not isinstance(payload, dict):
|
||||
logger.bind(tag=TAG).error("MCP消息缺少payload字段或格式错误")
|
||||
@@ -148,7 +148,7 @@ async def handle_mcp_message(conn, mcp_client: MCPClient, payload: dict, transpo
|
||||
if isinstance(server_info, dict):
|
||||
name = server_info.get("name")
|
||||
version = server_info.get("version")
|
||||
logger.bind(tag=TAG).info(
|
||||
logger.bind(tag=TAG).debug(
|
||||
f"客户端MCP服务器信息: name={name}, version={version}"
|
||||
)
|
||||
return
|
||||
@@ -205,11 +205,11 @@ async def handle_mcp_message(conn, mcp_client: MCPClient, payload: dict, transpo
|
||||
|
||||
next_cursor = result.get("nextCursor", "")
|
||||
if next_cursor:
|
||||
logger.bind(tag=TAG).info(f"有更多工具,nextCursor: {next_cursor}")
|
||||
logger.bind(tag=TAG).debug(f"有更多工具,nextCursor: {next_cursor}")
|
||||
await send_mcp_tools_list_continue_request(conn, next_cursor, transport)
|
||||
else:
|
||||
await mcp_client.set_ready(True)
|
||||
logger.bind(tag=TAG).info("所有工具已获取,MCP客户端准备就绪")
|
||||
logger.bind(tag=TAG).debug("所有工具已获取,MCP客户端准备就绪")
|
||||
|
||||
# 刷新工具缓存,确保MCP工具被包含在函数列表中
|
||||
if hasattr(conn, "func_handler") and conn.func_handler:
|
||||
@@ -265,7 +265,7 @@ async def send_mcp_initialize_message(conn, transport=None):
|
||||
},
|
||||
},
|
||||
}
|
||||
logger.bind(tag=TAG).info("发送MCP初始化消息")
|
||||
logger.bind(tag=TAG).debug("发送MCP初始化消息")
|
||||
await send_mcp_message(conn, payload, transport)
|
||||
|
||||
|
||||
|
||||
@@ -358,7 +358,9 @@ async def call_mcp_endpoint_tool(
|
||||
}
|
||||
|
||||
message = json.dumps(payload)
|
||||
logger.bind(tag=TAG).info(f"发送MCP接入点工具调用请求: {actual_name},参数: {args}")
|
||||
logger.bind(tag=TAG).info(
|
||||
f"发送MCP接入点工具调用请求: {actual_name},参数: {json.dumps(arguments, ensure_ascii=False)}"
|
||||
)
|
||||
await mcp_client.send_message(message)
|
||||
|
||||
try:
|
||||
|
||||
@@ -10,9 +10,13 @@ import concurrent.futures
|
||||
from contextlib import AsyncExitStack
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp import ClientSession, StdioServerParameters, Implementation
|
||||
from mcp.client.session import SamplingFnT, ElicitationFnT, ListRootsFnT, LoggingFnT, MessageHandlerFnT
|
||||
from mcp.client.stdio import stdio_client
|
||||
from mcp.client.sse import sse_client
|
||||
from mcp.client.streamable_http import streamablehttp_client
|
||||
from mcp.shared.session import ProgressFnT
|
||||
|
||||
from config.logger import setup_logging
|
||||
from core.utils.util import sanitize_tool_name
|
||||
|
||||
@@ -40,13 +44,25 @@ class ServerMCPClient:
|
||||
self.tools_dict: Dict[str, Any] = {}
|
||||
self.name_mapping: Dict[str, str] = {}
|
||||
|
||||
async def initialize(self):
|
||||
async def initialize(self, read_timeout_seconds: timedelta | None = None,
|
||||
sampling_callback: SamplingFnT | None = None,
|
||||
elicitation_callback: ElicitationFnT | None = None,
|
||||
list_roots_callback: ListRootsFnT | None = None,
|
||||
logging_callback: LoggingFnT | None = None,
|
||||
message_handler: MessageHandlerFnT | None = None,
|
||||
client_info: Implementation | None = None):
|
||||
"""初始化MCP客户端连接"""
|
||||
if self._worker_task:
|
||||
return
|
||||
|
||||
self._worker_task = asyncio.create_task(
|
||||
self._worker(), name="ServerMCPClientWorker"
|
||||
self._worker(read_timeout_seconds=read_timeout_seconds,
|
||||
sampling_callback=sampling_callback,
|
||||
elicitation_callback=elicitation_callback,
|
||||
list_roots_callback=list_roots_callback,
|
||||
logging_callback=logging_callback,
|
||||
message_handler=message_handler,
|
||||
client_info=client_info), name="ServerMCPClientWorker"
|
||||
)
|
||||
await self._ready_evt.wait()
|
||||
|
||||
@@ -96,12 +112,15 @@ class ServerMCPClient:
|
||||
for name, tool in self.tools_dict.items()
|
||||
]
|
||||
|
||||
async def call_tool(self, name: str, args: dict) -> Any:
|
||||
async def call_tool(self, name: str, arguments: dict, read_timeout_seconds: timedelta | None = None, progress_callback: ProgressFnT | None = None, *, meta: dict[str, Any] | None = None) -> Any:
|
||||
"""调用指定工具
|
||||
|
||||
Args:
|
||||
name: 工具名称
|
||||
args: 工具参数
|
||||
arguments: 工具参数
|
||||
read_timeout_seconds:
|
||||
progress_callback: 进度回调函数
|
||||
meta:
|
||||
|
||||
Returns:
|
||||
Any: 工具执行结果
|
||||
@@ -114,7 +133,7 @@ class ServerMCPClient:
|
||||
|
||||
real_name = self.name_mapping.get(name, name)
|
||||
loop = self._worker_task.get_loop()
|
||||
coro = self.session.call_tool(real_name, args)
|
||||
coro = self.session.call_tool(real_name, arguments=arguments, read_timeout_seconds=read_timeout_seconds, progress_callback=progress_callback, meta=meta)
|
||||
|
||||
if loop is asyncio.get_running_loop():
|
||||
return await coro
|
||||
@@ -143,7 +162,13 @@ class ServerMCPClient:
|
||||
# 所有检查都通过,连接正常
|
||||
return True
|
||||
|
||||
async def _worker(self):
|
||||
async def _worker(self, read_timeout_seconds: timedelta | None = None,
|
||||
sampling_callback: SamplingFnT | None = None,
|
||||
elicitation_callback: ElicitationFnT | None = None,
|
||||
list_roots_callback: ListRootsFnT | None = None,
|
||||
logging_callback: LoggingFnT | None = None,
|
||||
message_handler: MessageHandlerFnT | None = None,
|
||||
client_info: Implementation | None = None):
|
||||
"""MCP客户端工作协程"""
|
||||
async with AsyncExitStack() as stack:
|
||||
try:
|
||||
@@ -167,16 +192,38 @@ class ServerMCPClient:
|
||||
|
||||
# 建立SSEClient
|
||||
elif "url" in self.config:
|
||||
headers = dict(self.config.get("headers", {}))
|
||||
# TODO 兼容旧版本
|
||||
if "API_ACCESS_TOKEN" in self.config:
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.config['API_ACCESS_TOKEN']}"
|
||||
}
|
||||
headers["Authorization"] = f"Bearer {self.config['API_ACCESS_TOKEN']}"
|
||||
self.logger.bind(tag=TAG).warning(f"你正在使用旧过时的配置 API_ACCESS_TOKEN ,请在.mcp_server_settings.json中将API_ACCESS_TOKEN直接设置在headers中,例如 'Authorization': 'Bearer API_ACCESS_TOKEN'")
|
||||
|
||||
# 根据transport类型选择不同的客户端,默认为SSE
|
||||
transport_type = self.config.get("transport", "sse")
|
||||
|
||||
if transport_type == "streamable-http" or transport_type == "http":
|
||||
# 使用 Streamable HTTP 传输
|
||||
http_r, http_w, get_session_id = await stack.enter_async_context(
|
||||
streamablehttp_client(
|
||||
url=self.config["url"],
|
||||
headers=headers,
|
||||
timeout=self.config.get("timeout", 30),
|
||||
sse_read_timeout=self.config.get("sse_read_timeout", 60 * 5),
|
||||
terminate_on_close=self.config.get("terminate_on_close", True)
|
||||
)
|
||||
)
|
||||
read_stream, write_stream = http_r, http_w
|
||||
else:
|
||||
headers = {}
|
||||
sse_r, sse_w = await stack.enter_async_context(
|
||||
sse_client(self.config["url"], headers=headers)
|
||||
)
|
||||
read_stream, write_stream = sse_r, sse_w
|
||||
# 使用传统的 SSE 传输
|
||||
sse_r, sse_w = await stack.enter_async_context(
|
||||
sse_client(
|
||||
url=self.config["url"],
|
||||
headers=headers,
|
||||
timeout=self.config.get("timeout", 5),
|
||||
sse_read_timeout=self.config.get("sse_read_timeout", 60 * 5)
|
||||
)
|
||||
)
|
||||
read_stream, write_stream = sse_r, sse_w
|
||||
|
||||
else:
|
||||
raise ValueError("MCP客户端配置必须包含'command'或'url'")
|
||||
@@ -185,7 +232,13 @@ class ServerMCPClient:
|
||||
ClientSession(
|
||||
read_stream=read_stream,
|
||||
write_stream=write_stream,
|
||||
read_timeout_seconds=timedelta(seconds=15),
|
||||
read_timeout_seconds=read_timeout_seconds,
|
||||
sampling_callback=sampling_callback,
|
||||
elicitation_callback=elicitation_callback,
|
||||
list_roots_callback=list_roots_callback,
|
||||
logging_callback=logging_callback,
|
||||
message_handler=message_handler,
|
||||
client_info=client_info
|
||||
)
|
||||
)
|
||||
await self.session.initialize()
|
||||
|
||||
@@ -4,6 +4,9 @@ import asyncio
|
||||
import os
|
||||
import json
|
||||
from typing import Dict, Any, List
|
||||
|
||||
from mcp.types import LoggingMessageNotificationParams
|
||||
|
||||
from config.config_loader import get_project_dir
|
||||
from config.logger import setup_logging
|
||||
from .mcp_client import ServerMCPClient
|
||||
@@ -26,6 +29,7 @@ class ServerMCPManager:
|
||||
)
|
||||
self.clients: Dict[str, ServerMCPClient] = {}
|
||||
self.tools = []
|
||||
self._init_lock = asyncio.Lock()
|
||||
|
||||
def load_config(self) -> Dict[str, Any]:
|
||||
"""加载MCP服务配置"""
|
||||
@@ -42,29 +46,50 @@ class ServerMCPManager:
|
||||
)
|
||||
return {}
|
||||
|
||||
async def _init_server(self, name: str, srv_config: Dict[str, Any]):
|
||||
"""初始化单个MCP服务"""
|
||||
client = None
|
||||
try:
|
||||
# 初始化服务端MCP客户端
|
||||
logger.bind(tag=TAG).info(f"初始化服务端MCP客户端: {name}")
|
||||
client = ServerMCPClient(srv_config)
|
||||
# 设置超时时间10秒
|
||||
await asyncio.wait_for(client.initialize(logging_callback=self.logging_callback), timeout=10)
|
||||
|
||||
# 使用锁保护共享状态的修改
|
||||
async with self._init_lock:
|
||||
self.clients[name] = client
|
||||
client_tools = client.get_available_tools()
|
||||
self.tools.extend(client_tools)
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
logger.bind(tag=TAG).error(
|
||||
f"Failed to initialize MCP server {name}: Timeout"
|
||||
)
|
||||
if client:
|
||||
await client.cleanup()
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(
|
||||
f"Failed to initialize MCP server {name}: {e}"
|
||||
)
|
||||
if client:
|
||||
await client.cleanup()
|
||||
|
||||
async def initialize_servers(self) -> None:
|
||||
"""初始化所有MCP服务"""
|
||||
config = self.load_config()
|
||||
tasks = []
|
||||
for name, srv_config in config.items():
|
||||
if not srv_config.get("command") and not srv_config.get("url"):
|
||||
logger.bind(tag=TAG).warning(
|
||||
f"Skipping server {name}: neither command nor url specified"
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
# 初始化服务端MCP客户端
|
||||
logger.bind(tag=TAG).info(f"初始化服务端MCP客户端: {name}")
|
||||
client = ServerMCPClient(srv_config)
|
||||
await client.initialize()
|
||||
self.clients[name] = client
|
||||
client_tools = client.get_available_tools()
|
||||
self.tools.extend(client_tools)
|
||||
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(
|
||||
f"Failed to initialize MCP server {name}: {e}"
|
||||
)
|
||||
|
||||
tasks.append(self._init_server(name, srv_config))
|
||||
|
||||
if tasks:
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
# 输出当前支持的服务端MCP工具列表
|
||||
if hasattr(self.conn, "func_handler") and self.conn.func_handler:
|
||||
@@ -109,7 +134,7 @@ class ServerMCPManager:
|
||||
# 带重试机制的工具调用
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
return await target_client.call_tool(tool_name, arguments)
|
||||
return await target_client.call_tool(tool_name, arguments, progress_callback=self.progress_callback)
|
||||
except Exception as e:
|
||||
# 最后一次尝试失败时直接抛出异常
|
||||
if attempt == max_retries - 1:
|
||||
@@ -131,7 +156,7 @@ class ServerMCPManager:
|
||||
config = self.load_config()
|
||||
if client_name in config:
|
||||
client = ServerMCPClient(config[client_name])
|
||||
await client.initialize()
|
||||
await client.initialize(logging_callback=self.logging_callback)
|
||||
self.clients[client_name] = client
|
||||
target_client = client
|
||||
logger.bind(tag=TAG).info(
|
||||
@@ -159,3 +184,11 @@ class ServerMCPManager:
|
||||
except (asyncio.TimeoutError, Exception) as e:
|
||||
logger.bind(tag=TAG).error(f"关闭服务端MCP客户端 {name} 时出错: {e}")
|
||||
self.clients.clear()
|
||||
|
||||
# 可选回调方法
|
||||
|
||||
async def logging_callback(self, params: LoggingMessageNotificationParams):
|
||||
logger.bind(tag=TAG).info(f"[Server Log - {params.level.upper()}] {params.data}")
|
||||
|
||||
async def progress_callback(self, progress: float, total: float | None, message: str | None) -> None:
|
||||
logger.bind(tag=TAG).info(f"[Progress {progress}/{total}]: {message}")
|
||||
@@ -71,6 +71,19 @@ class ServerPluginExecutor(ToolExecutor):
|
||||
for func_name in all_required_functions:
|
||||
func_item = all_function_registry.get(func_name)
|
||||
if func_item:
|
||||
# 从函数注册中获取描述
|
||||
fun_description = (
|
||||
self.config.get("plugins", {})
|
||||
.get(func_name, {})
|
||||
.get("description", "")
|
||||
)
|
||||
if fun_description is not None and len(fun_description) > 0:
|
||||
if "function" in func_item.description and isinstance(
|
||||
func_item.description["function"], dict
|
||||
):
|
||||
func_item.description["function"][
|
||||
"description"
|
||||
] = fun_description
|
||||
tools[func_name] = ToolDefinition(
|
||||
name=func_name,
|
||||
description=func_item.description,
|
||||
|
||||
@@ -69,7 +69,7 @@ class UnifiedToolHandler:
|
||||
self._initialize_home_assistant()
|
||||
|
||||
self.finish_init = True
|
||||
self.logger.info("统一工具处理器初始化完成")
|
||||
self.logger.debug("统一工具处理器初始化完成")
|
||||
|
||||
# 输出当前支持的所有工具列表
|
||||
self.current_support_functions()
|
||||
|
||||
@@ -20,7 +20,7 @@ class ToolManager:
|
||||
"""注册工具执行器"""
|
||||
self.executors[tool_type] = executor
|
||||
self._invalidate_cache()
|
||||
self.logger.info(f"注册工具执行器: {tool_type.value}")
|
||||
self.logger.debug(f"注册工具执行器: {tool_type.value}")
|
||||
|
||||
def _invalidate_cache(self):
|
||||
"""使缓存失效"""
|
||||
@@ -109,7 +109,7 @@ class ToolManager:
|
||||
def refresh_tools(self):
|
||||
"""刷新工具缓存"""
|
||||
self._invalidate_cache()
|
||||
self.logger.info("工具缓存已刷新")
|
||||
self.logger.debug("工具缓存已刷新")
|
||||
|
||||
def get_tool_statistics(self) -> Dict[str, int]:
|
||||
"""获取工具统计信息"""
|
||||
|
||||
Reference in New Issue
Block a user