Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0de7ae8
fix: resolve MCP tools race condition causing 'completion 无法解析' error
idiotsj Feb 27, 2026
ec9f740
perf: add timeout and better error handling for MCP initialization
idiotsj Feb 27, 2026
4924739
refactor: simplify MCP init orchestration and improve log security
idiotsj Feb 27, 2026
2cfe428
fix: prevent initialized MCP clients from being cleaned up on timeout
idiotsj Feb 27, 2026
38e99cf
fix: restore task cancellation on timeout per review feedback
idiotsj Feb 27, 2026
5fab8aa
fix: separate init signal from client lifetime in MCP task wrapper
idiotsj Mar 1, 2026
b810dfb
security: redact sensitive MCP config from debug logs
idiotsj Mar 1, 2026
e2da013
refactor: use McpClientInfo dataclass and MCP_INIT_TIMEOUT constant
idiotsj Mar 1, 2026
df1e9f3
fix: handle CancelledError and clean up wait_tasks on timeout
idiotsj Mar 1, 2026
fdcbdd4
fix: align enable_mcp_server with new wrapper API and fix security/co…
idiotsj Mar 1, 2026
a16d05c
refactor: register mcp_client_event only after successful init in ena…
idiotsj Mar 1, 2026
a058bef
fix: harden MCP init state handling and timeout parsing
zouyonghe Mar 2, 2026
7b40501
fix: improve MCP timeout and post-init error observability
zouyonghe Mar 2, 2026
072a75d
refactor: simplify MCP init lifecycle orchestration
zouyonghe Mar 2, 2026
55a64d6
refactor: simplify MCP init flow and cap timeout values
zouyonghe Mar 2, 2026
7f39a0a
fix: refine mcp timeout handling and lifecycle task tracking
zouyonghe Mar 2, 2026
807fb4a
fix: harden mcp shutdown and timeout source logging
zouyonghe Mar 2, 2026
88274d9
refactor: simplify mcp runtime registry and timeout flow
zouyonghe Mar 2, 2026
a8e0b9d
fix: keep mcp init summary return contract
zouyonghe Mar 2, 2026
09e6174
refactor: streamline mcp lifecycle and init errors
zouyonghe Mar 2, 2026
6902a99
refactor: unify mcp lifecycle wait handling
zouyonghe Mar 2, 2026
bcab236
refactor: simplify mcp runtime ownership and timeout resolution
zouyonghe Mar 2, 2026
9524878
fix: harden mcp shutdown waiting and startup signaling
zouyonghe Mar 2, 2026
9221f3b
refactor: streamline mcp lifecycle and shutdown errors
idiotsj Mar 2, 2026
24b8379
refactor: harden mcp runtime access and shutdown
idiotsj Mar 2, 2026
e69ff0c
fix: ensure mcp client cleanup and clarify views
idiotsj Mar 2, 2026
146c743
refactor: cache mcp client view and guard startup
idiotsj Mar 2, 2026
424aa5b
refactor: simplify mcp init cleanup and runtime lock
idiotsj Mar 2, 2026
ef8e254
refactor: reduce mcp runtime duplication
idiotsj Mar 2, 2026
1e93ce4
refactor: reuse mcp cleanup and client view
idiotsj Mar 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 60 additions & 13 deletions astrbot/core/provider/func_tool_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import copy
import json
import os
import urllib.parse
from collections.abc import AsyncGenerator, Awaitable, Callable
from typing import Any

Expand Down Expand Up @@ -212,37 +213,83 @@ async def init_mcp_clients(self) -> None:
open(mcp_json_file, encoding="utf-8"),
)["mcpServers"]

for name in mcp_server_json_obj:
cfg = mcp_server_json_obj[name]
tasks: dict[str, asyncio.Task] = {}
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated

for name, cfg in mcp_server_json_obj.items():
if cfg.get("active", True):
event = asyncio.Event()
asyncio.create_task(
task = asyncio.create_task(
self._init_mcp_client_task_wrapper(name, cfg, event),
)
tasks[name] = task
self.mcp_client_event[name] = event

if tasks:
logger.info(f"等待 {len(tasks)} 个 MCP 服务初始化...")

done, pending = await asyncio.wait(tasks.values(), timeout=20.0)

if pending:
logger.warning(
"MCP 服务初始化超时(20秒),部分服务可能未完全加载。"
"建议检查 MCP 服务器配置和网络连接。"
)
for task in pending:
task.cancel()

success_count = 0
failed_services: list[str] = []

for name, task in tasks.items():
if task in pending:
logger.error(f"MCP 服务 {name} 初始化超时")
failed_services.append(name)
continue

exc = task.exception()
if exc is not None:
logger.error(f"MCP 服务 {name} 初始化失败: {exc}")
# 仅在 debug 级别输出完整配置,避免在生产日志中泄露敏感信息
cfg = mcp_server_json_obj.get(name, {})
if "command" in cfg:
logger.debug(f" 命令: {cfg['command']}")
if "args" in cfg:
logger.debug(f" 参数: {cfg['args']}")
elif "url" in cfg:
parsed = urllib.parse.urlparse(cfg["url"])
logger.debug(f" 主机: {parsed.scheme}://{parsed.netloc}")
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated
failed_services.append(name)
else:
success_count += 1

if failed_services:
logger.warning(
f"以下 MCP 服务初始化失败: {', '.join(failed_services)}。"
f"请检查配置文件 mcp_server.json 和服务器可用性。"
)

logger.info(f"MCP 服务初始化完成: {success_count}/{len(tasks)} 成功")

async def _init_mcp_client_task_wrapper(
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated
self,
name: str,
cfg: dict,
event: asyncio.Event,
ready_future: asyncio.Future | None = None,
) -> None:
"""初始化 MCP 客户端的包装函数,用于捕获异常"""
initialized = False
try:
await self._init_mcp_client(name, cfg)
tools = await self.mcp_client_dict[name].list_tools_and_save()
if ready_future and not ready_future.done():
# tell the caller we are ready
ready_future.set_result(tools)
initialized = True
await event.wait()
logger.info(f"收到 MCP 客户端 {name} 终止信号")
except Exception as e:
logger.error(f"初始化 MCP 客户端 {name} 失败", exc_info=True)
if ready_future and not ready_future.done():
ready_future.set_exception(e)
except Exception:
if not initialized:
# 初始化阶段失败,记录错误并向上抛出让 task.exception() 捕获
logger.error(f"初始化 MCP 客户端 {name} 失败", exc_info=True)
raise
# 初始化已成功,此处异常来自 event.wait() 被取消,属于正常终止流程
finally:
# 无论如何都能清理
await self._terminate_mcp_client(name)

async def _init_mcp_client(self, name: str, config: dict) -> None:
Expand Down
4 changes: 2 additions & 2 deletions astrbot/core/provider/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,8 +274,8 @@ async def initialize(self):
if not self.curr_tts_provider_inst and self.tts_provider_insts:
self.curr_tts_provider_inst = self.tts_provider_insts[0]

# 初始化 MCP Client 连接
asyncio.create_task(self.llm_tools.init_mcp_clients(), name="init_mcp_clients")
# 初始化 MCP Client 连接(等待完成以确保工具可用)
await self.llm_tools.init_mcp_clients()

def dynamic_import_provider(self, type: str):
"""动态导入提供商适配器模块
Expand Down