Skip to content

Commit ad562b6

Browse files
zouyonghestevessr
authored andcommitted
[codex] fix mcp init timeout keyword mismatch (AstrBotDevs#5743)
* fix: use timeout_seconds for mcp init startup * fix: support overridden mcp init timeout in startup * fix: resolve mcp init timeout from env when unset * fix: pass mcp init timeout through lifecycle chain
1 parent ca63358 commit ad562b6

5 files changed

Lines changed: 125 additions & 10 deletions

File tree

astrbot/core/core_lifecycle.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,11 @@ async def _init_or_reload_subagent_orchestrator(self) -> None:
9797
except Exception as e:
9898
logger.error(f"Subagent orchestrator init failed: {e}", exc_info=True)
9999

100-
async def initialize(self) -> None:
100+
async def initialize(
101+
self,
102+
*,
103+
mcp_init_timeout: float | int | str | None = None,
104+
) -> None:
101105
"""初始化 AstrBot 核心生命周期管理类.
102106
103107
负责初始化各个组件, 包括 ProviderManager、PlatformManager、ConversationManager、PluginManager、PipelineScheduler、EventBus、AstrBotUpdator等。
@@ -201,7 +205,7 @@ async def initialize(self) -> None:
201205
await self.plugin_manager.reload()
202206

203207
# 根据配置实例化各个 Provider
204-
await self.provider_manager.initialize()
208+
await self.provider_manager.initialize(init_timeout=mcp_init_timeout)
205209

206210
await self.kb_manager.initialize()
207211

astrbot/core/provider/func_tool_manager.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,10 @@ def _log_safe_mcp_debug_config(cfg: dict) -> None:
347347
logger.debug(f" 主机: {scheme}://{host}{port}")
348348

349349
async def init_mcp_clients(
350-
self, raise_on_all_failed: bool = False
350+
self,
351+
raise_on_all_failed: bool = False,
352+
*,
353+
init_timeout: float | int | str | None = None,
351354
) -> MCPInitSummary:
352355
"""从项目根目录读取 mcp_server.json 文件,初始化 MCP 服务列表。文件格式如下:
353356
```
@@ -368,6 +371,7 @@ async def init_mcp_clients(
368371
```
369372
370373
Timeout behavior:
374+
- 显式 `init_timeout` 参数优先(用于测试或调用方覆盖)。
371375
- 初始化超时使用环境变量 ASTRBOT_MCP_INIT_TIMEOUT 或默认值。
372376
- 动态启用超时使用 ASTRBOT_MCP_ENABLE_TIMEOUT(独立于初始化超时)。
373377
"""
@@ -393,8 +397,12 @@ async def init_mcp_clients(
393397
"mcpServers"
394398
]
395399

396-
init_timeout = self._init_timeout_default
397-
timeout_display = f"{init_timeout:g}"
400+
init_timeout_value = _resolve_timeout(
401+
timeout=init_timeout,
402+
env_name=MCP_INIT_TIMEOUT_ENV,
403+
default=self._init_timeout_default,
404+
)
405+
timeout_display = f"{init_timeout_value:g}"
398406

399407
active_configs: list[tuple[str, dict, asyncio.Event]] = []
400408
for name, cfg in mcp_server_json_obj.items():
@@ -413,7 +421,7 @@ async def init_mcp_clients(
413421
name=name,
414422
cfg=cfg,
415423
shutdown_event=shutdown_event,
416-
timeout=init_timeout,
424+
timeout_seconds=init_timeout_value,
417425
),
418426
name=f"mcp-init:{name}",
419427
)

astrbot/core/provider/manager.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,11 @@ def get_using_provider(
269269

270270
return provider
271271

272-
async def initialize(self) -> None:
272+
async def initialize(
273+
self,
274+
*,
275+
init_timeout: float | int | str | None = None,
276+
) -> None:
273277
# 逐个初始化提供商
274278
for provider_config in self.providers_config:
275279
try:
@@ -338,7 +342,8 @@ async def initialize(self) -> None:
338342
"on",
339343
}
340344
mcp_init_summary = await self.llm_tools.init_mcp_clients(
341-
raise_on_all_failed=strict_mcp_init
345+
raise_on_all_failed=strict_mcp_init,
346+
init_timeout=init_timeout,
342347
)
343348
if (
344349
mcp_init_summary.total > 0

tests/unit/test_core_lifecycle.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ async def test_initialize_sets_up_all_components(
373373
new_callable=AsyncMock,
374374
),
375375
):
376-
await lifecycle.initialize()
376+
await lifecycle.initialize(mcp_init_timeout=3.5)
377377

378378
# Verify database initialized
379379
mock_db.initialize.assert_awaited_once()
@@ -388,7 +388,7 @@ async def test_initialize_sets_up_all_components(
388388
mock_persona_mgr.initialize.assert_awaited_once()
389389

390390
# Verify provider manager initialized
391-
mock_provider_manager.initialize.assert_awaited_once()
391+
mock_provider_manager.initialize.assert_awaited_once_with(init_timeout=3.5)
392392

393393
# Verify platform manager initialized
394394
mock_platform_manager.initialize.assert_awaited_once()
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import json
2+
3+
import pytest
4+
5+
from astrbot.core.provider import func_tool_manager
6+
from astrbot.core.provider.func_tool_manager import FunctionToolManager
7+
8+
9+
@pytest.fixture
10+
def mcp_init_harness(
11+
monkeypatch: pytest.MonkeyPatch,
12+
tmp_path,
13+
):
14+
manager = FunctionToolManager()
15+
data_dir = tmp_path / "data"
16+
data_dir.mkdir()
17+
18+
(data_dir / "mcp_server.json").write_text(
19+
json.dumps({"mcpServers": {"demo": {"active": True}}}),
20+
encoding="utf-8",
21+
)
22+
monkeypatch.setattr(
23+
func_tool_manager,
24+
"get_astrbot_data_path",
25+
lambda: data_dir,
26+
)
27+
28+
called = {}
29+
30+
async def fake_start_mcp_server(*, name, cfg, shutdown_event, timeout_seconds):
31+
called[name] = {
32+
"cfg": cfg,
33+
"shutdown_event_type": type(shutdown_event).__name__,
34+
"timeout_seconds": timeout_seconds,
35+
}
36+
37+
monkeypatch.setattr(manager, "_start_mcp_server", fake_start_mcp_server)
38+
return manager, called
39+
40+
41+
def assert_demo_init_result(summary, called, *, timeout_seconds: float) -> None:
42+
assert summary.total == 1
43+
assert summary.success == 1
44+
assert summary.failed == []
45+
assert called["demo"]["cfg"] == {"active": True}
46+
assert called["demo"]["shutdown_event_type"] == "Event"
47+
assert called["demo"]["timeout_seconds"] == timeout_seconds
48+
49+
50+
@pytest.mark.asyncio
51+
async def test_init_mcp_clients_passes_timeout_seconds_keyword(mcp_init_harness):
52+
manager, called = mcp_init_harness
53+
54+
summary = await manager.init_mcp_clients()
55+
56+
assert_demo_init_result(
57+
summary,
58+
called,
59+
timeout_seconds=manager._init_timeout_default,
60+
)
61+
62+
63+
@pytest.mark.asyncio
64+
async def test_init_mcp_clients_passes_overridden_init_timeout(
65+
mcp_init_harness,
66+
):
67+
manager, called = mcp_init_harness
68+
69+
summary = await manager.init_mcp_clients(init_timeout=3.5)
70+
71+
assert_demo_init_result(summary, called, timeout_seconds=3.5)
72+
73+
74+
@pytest.mark.asyncio
75+
async def test_init_mcp_clients_reads_env_timeout_when_not_overridden(
76+
mcp_init_harness,
77+
monkeypatch: pytest.MonkeyPatch,
78+
):
79+
manager, called = mcp_init_harness
80+
manager._init_timeout_default = 20.0 # ensure env override is observable
81+
monkeypatch.setenv("ASTRBOT_MCP_INIT_TIMEOUT", "3.5")
82+
83+
summary = await manager.init_mcp_clients()
84+
85+
assert_demo_init_result(summary, called, timeout_seconds=3.5)
86+
87+
88+
@pytest.mark.asyncio
89+
async def test_init_mcp_clients_prefers_explicit_timeout_over_env(
90+
mcp_init_harness,
91+
monkeypatch: pytest.MonkeyPatch,
92+
):
93+
manager, called = mcp_init_harness
94+
monkeypatch.setenv("ASTRBOT_MCP_INIT_TIMEOUT", "7.0")
95+
96+
summary = await manager.init_mcp_clients(init_timeout=3.5)
97+
98+
assert_demo_init_result(summary, called, timeout_seconds=3.5)

0 commit comments

Comments
 (0)