resolve merge conflict

This commit is contained in:
caixypromise
2025-12-29 00:09:26 +08:00
375 changed files with 45358 additions and 9704 deletions
@@ -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]:
"""获取工具统计信息"""