Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
20 changes: 16 additions & 4 deletions astrbot/core/provider/func_tool_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,10 @@ def _log_safe_mcp_debug_config(cfg: dict) -> None:
logger.debug(f" 主机: {scheme}://{host}{port}")

async def init_mcp_clients(
self, raise_on_all_failed: bool = False
self,
raise_on_all_failed: bool = False,
*,
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
init_timeout: float | int | str | None = None,
) -> MCPInitSummary:
"""从项目根目录读取 mcp_server.json 文件,初始化 MCP 服务列表。文件格式如下:
```
Expand All @@ -368,6 +371,7 @@ async def init_mcp_clients(
```

Timeout behavior:
- 显式 `init_timeout` 参数优先(用于测试或调用方覆盖)。
- 初始化超时使用环境变量 ASTRBOT_MCP_INIT_TIMEOUT 或默认值。
- 动态启用超时使用 ASTRBOT_MCP_ENABLE_TIMEOUT(独立于初始化超时)。
"""
Expand All @@ -393,8 +397,16 @@ async def init_mcp_clients(
"mcpServers"
]

init_timeout = self._init_timeout_default
timeout_display = f"{init_timeout:g}"
init_timeout_value = (
self._init_timeout_default
if init_timeout is None
else _resolve_timeout(
timeout=init_timeout,
env_name=MCP_INIT_TIMEOUT_ENV,
default=self._init_timeout_default,
)
)
timeout_display = f"{init_timeout_value:g}"

active_configs: list[tuple[str, dict, asyncio.Event]] = []
for name, cfg in mcp_server_json_obj.items():
Expand All @@ -413,7 +425,7 @@ async def init_mcp_clients(
name=name,
cfg=cfg,
shutdown_event=shutdown_event,
timeout=init_timeout,
timeout_seconds=init_timeout_value,
),
name=f"mcp-init:{name}",
)
Expand Down
86 changes: 86 additions & 0 deletions tests/unit/test_func_tool_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import json

import pytest

from astrbot.core.provider import func_tool_manager
from astrbot.core.provider.func_tool_manager import FunctionToolManager


@pytest.mark.asyncio
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): 添加一个测试,用于验证在使用非默认的 init_timeout 值时的行为。

为了更好地覆盖此次回归,请同时测试一个非默认的超时时间(例如 await manager.init_mcp_clients(init_timeout=3.5)),并断言 called["demo"]["timeout_seconds"] 等于该值。这样可以验证当调用方覆盖超时时间时,该参数能够被正确传递。

Original comment in English

suggestion (testing): Add a test that exercises a non-default init_timeout value

To better cover the regression, please also test a non-default timeout (e.g. await manager.init_mcp_clients(init_timeout=3.5)) and assert that called["demo"]["timeout_seconds"] equals that value. This will validate the parameter is correctly forwarded when callers override the timeout.

async def test_init_mcp_clients_passes_timeout_seconds_keyword(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
):
manager = FunctionToolManager()
data_dir = tmp_path / "data"
data_dir.mkdir()

(data_dir / "mcp_server.json").write_text(
json.dumps({"mcpServers": {"demo": {"active": True}}}),
encoding="utf-8",
)
monkeypatch.setattr(
func_tool_manager,
"get_astrbot_data_path",
lambda: data_dir,
)

called = {}

async def fake_start_mcp_server(*, name, cfg, shutdown_event, timeout_seconds):
called[name] = {
"cfg": cfg,
"shutdown_event_type": type(shutdown_event).__name__,
"timeout_seconds": timeout_seconds,
}

monkeypatch.setattr(manager, "_start_mcp_server", fake_start_mcp_server)

summary = await manager.init_mcp_clients()

assert summary.total == 1
assert summary.success == 1
assert summary.failed == []
assert called["demo"]["cfg"] == {"active": True}
assert called["demo"]["shutdown_event_type"] == "Event"
assert called["demo"]["timeout_seconds"] == manager._init_timeout_default


@pytest.mark.asyncio
async def test_init_mcp_clients_passes_overridden_init_timeout(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
):
manager = FunctionToolManager()
data_dir = tmp_path / "data"
data_dir.mkdir()

(data_dir / "mcp_server.json").write_text(
json.dumps({"mcpServers": {"demo": {"active": True}}}),
encoding="utf-8",
)
monkeypatch.setattr(
func_tool_manager,
"get_astrbot_data_path",
lambda: data_dir,
)

called = {}

async def fake_start_mcp_server(*, name, cfg, shutdown_event, timeout_seconds):
called[name] = {
"cfg": cfg,
"shutdown_event_type": type(shutdown_event).__name__,
"timeout_seconds": timeout_seconds,
}

monkeypatch.setattr(manager, "_start_mcp_server", fake_start_mcp_server)

summary = await manager.init_mcp_clients(init_timeout=3.5)

assert summary.total == 1
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Cover precedence when both init_timeout and ASTRBOT_MCP_INIT_TIMEOUT are set

你已经覆盖了只传显式 init_timeout 和只用环境变量这两种情况。请再添加一个测试:同时设置 ASTRBOT_MCP_INIT_TIMEOUTinit_timeout,并断言显式参数优先生效。这样可以直接验证文档描述的行为,并防止 _resolve_timeout 或其调用点在将来出现回归。

Suggested change
@pytest.mark.asyncio
async def test_init_mcp_clients_passes_overridden_init_timeout(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
):
manager = FunctionToolManager()
data_dir = tmp_path / "data"
data_dir.mkdir()
(data_dir / "mcp_server.json").write_text(
json.dumps({"mcpServers": {"demo": {"active": True}}}),
encoding="utf-8",
)
monkeypatch.setattr(
func_tool_manager,
"get_astrbot_data_path",
lambda: data_dir,
)
called = {}
async def fake_start_mcp_server(*, name, cfg, shutdown_event, timeout_seconds):
called[name] = {
"cfg": cfg,
"shutdown_event_type": type(shutdown_event).__name__,
"timeout_seconds": timeout_seconds,
}
monkeypatch.setattr(manager, "_start_mcp_server", fake_start_mcp_server)
summary = await manager.init_mcp_clients(init_timeout=3.5)
assert summary.total == 1
@pytest.mark.asyncio
async def test_init_mcp_clients_passes_overridden_init_timeout(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
):
manager = FunctionToolManager()
data_dir = tmp_path / "data"
data_dir.mkdir()
(data_dir / "mcp_server.json").write_text(
json.dumps({"mcpServers": {"demo": {"active": True}}}),
encoding="utf-8",
)
monkeypatch.setattr(
func_tool_manager,
"get_astrbot_data_path",
lambda: data_dir,
)
called = {}
async def fake_start_mcp_server(*, name, cfg, shutdown_event, timeout_seconds):
called[name] = {
"cfg": cfg,
"shutdown_event_type": type(shutdown_event).__name__,
"timeout_seconds": timeout_seconds,
}
monkeypatch.setattr(manager, "_start_mcp_server", fake_start_mcp_server)
summary = await manager.init_mcp_clients(init_timeout=3.5)
assert summary.total == 1
@pytest.mark.asyncio
async def test_init_mcp_clients_prefers_explicit_timeout_over_env(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
):
manager = FunctionToolManager()
data_dir = tmp_path / "data"
data_dir.mkdir()
(data_dir / "mcp_server.json").write_text(
json.dumps({"mcpServers": {"demo": {"active": True}}}),
encoding="utf-8",
)
monkeypatch.setattr(
func_tool_manager,
"get_astrbot_data_path",
lambda: data_dir,
)
# Set an environment default that should be overridden by the explicit argument
monkeypatch.setenv("ASTRBOT_MCP_INIT_TIMEOUT", "7.0")
called = {}
async def fake_start_mcp_server(*, name, cfg, shutdown_event, timeout_seconds):
called[name] = {
"cfg": cfg,
"shutdown_event_type": type(shutdown_event).__name__,
"timeout_seconds": timeout_seconds,
}
monkeypatch.setattr(manager, "_start_mcp_server", fake_start_mcp_server)
summary = await manager.init_mcp_clients(init_timeout=3.5)
assert summary.total == 1
# Explicit argument should take precedence over the env var default
assert called["demo"]["timeout_seconds"] == 3.5
Original comment in English

suggestion (testing): Cover precedence when both init_timeout and ASTRBOT_MCP_INIT_TIMEOUT are set

You already cover the explicit init_timeout case and the env-var-only case. Please also add a test where both ASTRBOT_MCP_INIT_TIMEOUT and init_timeout are set, asserting that the explicit argument takes precedence. This will directly exercise the documented behavior and protect against regressions in _resolve_timeout or its call sites.

Suggested change
@pytest.mark.asyncio
async def test_init_mcp_clients_passes_overridden_init_timeout(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
):
manager = FunctionToolManager()
data_dir = tmp_path / "data"
data_dir.mkdir()
(data_dir / "mcp_server.json").write_text(
json.dumps({"mcpServers": {"demo": {"active": True}}}),
encoding="utf-8",
)
monkeypatch.setattr(
func_tool_manager,
"get_astrbot_data_path",
lambda: data_dir,
)
called = {}
async def fake_start_mcp_server(*, name, cfg, shutdown_event, timeout_seconds):
called[name] = {
"cfg": cfg,
"shutdown_event_type": type(shutdown_event).__name__,
"timeout_seconds": timeout_seconds,
}
monkeypatch.setattr(manager, "_start_mcp_server", fake_start_mcp_server)
summary = await manager.init_mcp_clients(init_timeout=3.5)
assert summary.total == 1
@pytest.mark.asyncio
async def test_init_mcp_clients_passes_overridden_init_timeout(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
):
manager = FunctionToolManager()
data_dir = tmp_path / "data"
data_dir.mkdir()
(data_dir / "mcp_server.json").write_text(
json.dumps({"mcpServers": {"demo": {"active": True}}}),
encoding="utf-8",
)
monkeypatch.setattr(
func_tool_manager,
"get_astrbot_data_path",
lambda: data_dir,
)
called = {}
async def fake_start_mcp_server(*, name, cfg, shutdown_event, timeout_seconds):
called[name] = {
"cfg": cfg,
"shutdown_event_type": type(shutdown_event).__name__,
"timeout_seconds": timeout_seconds,
}
monkeypatch.setattr(manager, "_start_mcp_server", fake_start_mcp_server)
summary = await manager.init_mcp_clients(init_timeout=3.5)
assert summary.total == 1
@pytest.mark.asyncio
async def test_init_mcp_clients_prefers_explicit_timeout_over_env(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
):
manager = FunctionToolManager()
data_dir = tmp_path / "data"
data_dir.mkdir()
(data_dir / "mcp_server.json").write_text(
json.dumps({"mcpServers": {"demo": {"active": True}}}),
encoding="utf-8",
)
monkeypatch.setattr(
func_tool_manager,
"get_astrbot_data_path",
lambda: data_dir,
)
# Set an environment default that should be overridden by the explicit argument
monkeypatch.setenv("ASTRBOT_MCP_INIT_TIMEOUT", "7.0")
called = {}
async def fake_start_mcp_server(*, name, cfg, shutdown_event, timeout_seconds):
called[name] = {
"cfg": cfg,
"shutdown_event_type": type(shutdown_event).__name__,
"timeout_seconds": timeout_seconds,
}
monkeypatch.setattr(manager, "_start_mcp_server", fake_start_mcp_server)
summary = await manager.init_mcp_clients(init_timeout=3.5)
assert summary.total == 1
# Explicit argument should take precedence over the env var default
assert called["demo"]["timeout_seconds"] == 3.5

assert summary.success == 1
assert summary.failed == []
assert called["demo"]["cfg"] == {"active": True}
assert called["demo"]["shutdown_event_type"] == "Event"
assert called["demo"]["timeout_seconds"] == 3.5