Skip to content

Commit 9ab6526

Browse files
authored
feat: 支持配置工具调用超时时间并适配 ModelScope 的 MCP Server 配置 (#3039)
* feat: 支持配置工具调用超时时间并适配 ModelScope 的 MCP Server 配置。 closes: #2939 * fix: Remove unnecessary blank lines in _quick_test_mcp_connection function
1 parent 9119f71 commit 9ab6526

8 files changed

Lines changed: 86 additions & 20 deletions

File tree

astrbot/core/agent/mcp_client.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,15 @@ async def _quick_test_mcp_connection(config: dict) -> tuple[bool, str]:
4040
timeout = cfg.get("timeout", 10)
4141

4242
try:
43+
if "transport" in cfg:
44+
transport_type = cfg["transport"]
45+
elif "type" in cfg:
46+
transport_type = cfg["type"]
47+
else:
48+
raise Exception("MCP 连接配置缺少 transport 或 type 字段")
49+
4350
async with aiohttp.ClientSession() as session:
44-
if cfg.get("transport") == "streamable_http":
51+
if transport_type == "streamable_http":
4552
test_payload = {
4653
"jsonrpc": "2.0",
4754
"method": "initialize",
@@ -121,7 +128,14 @@ def logging_callback(msg: str):
121128
if not success:
122129
raise Exception(error_msg)
123130

124-
if cfg.get("transport") != "streamable_http":
131+
if "transport" in cfg:
132+
transport_type = cfg["transport"]
133+
elif "type" in cfg:
134+
transport_type = cfg["type"]
135+
else:
136+
raise Exception("MCP 连接配置缺少 transport 或 type 字段")
137+
138+
if transport_type != "streamable_http":
125139
# SSE transport method
126140
self._streams_context = sse_client(
127141
url=cfg["url"],
@@ -134,7 +148,7 @@ def logging_callback(msg: str):
134148
)
135149

136150
# Create a new client session
137-
read_timeout = timedelta(seconds=cfg.get("session_read_timeout", 20))
151+
read_timeout = timedelta(seconds=cfg.get("session_read_timeout", 60))
138152
self.session = await self.exit_stack.enter_async_context(
139153
mcp.ClientSession(
140154
*streams,
@@ -159,7 +173,7 @@ def logging_callback(msg: str):
159173
)
160174

161175
# Create a new client session
162-
read_timeout = timedelta(seconds=cfg.get("session_read_timeout", 20))
176+
read_timeout = timedelta(seconds=cfg.get("session_read_timeout", 60))
163177
self.session = await self.exit_stack.enter_async_context(
164178
mcp.ClientSession(
165179
read_stream=read_s,

astrbot/core/astr_agent_context.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ class AstrAgentContext:
99
first_provider_request: ProviderRequest
1010
curr_provider_request: ProviderRequest
1111
streaming: bool
12+
tool_call_timeout: int = 60 # Default tool call timeout in seconds

astrbot/core/config/default.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"show_tool_use_status": False,
7373
"streaming_segmented": False,
7474
"max_agent_step": 30,
75+
"tool_call_timeout": 60,
7576
},
7677
"provider_stt_settings": {
7778
"enable": False,
@@ -1873,6 +1874,10 @@
18731874
"description": "工具调用轮数上限",
18741875
"type": "int",
18751876
},
1877+
"tool_call_timeout": {
1878+
"description": "工具调用超时时间(秒)",
1879+
"type": "int",
1880+
},
18761881
},
18771882
},
18781883
"provider_stt_settings": {
@@ -2145,6 +2150,10 @@
21452150
"description": "工具调用轮数上限",
21462151
"type": "int",
21472152
},
2153+
"provider_settings.tool_call_timeout": {
2154+
"description": "工具调用超时时间(秒)",
2155+
"type": "int",
2156+
},
21482157
"provider_settings.streaming_response": {
21492158
"description": "流式回复",
21502159
"type": "bool",

astrbot/core/pipeline/process_stage/method/llm_request.py

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import copy
77
import json
88
import traceback
9+
from datetime import timedelta
910
from typing import AsyncGenerator, Union
1011
from astrbot.core.conversation_mgr import Conversation
1112
from astrbot.core import logger
@@ -185,21 +186,33 @@ async def _execute_local(
185186
handler=awaitable,
186187
**tool_args,
187188
)
188-
async for resp in wrapper:
189-
if resp is not None:
190-
if isinstance(resp, mcp.types.CallToolResult):
191-
yield resp
189+
# async for resp in wrapper:
190+
while True:
191+
try:
192+
resp = await asyncio.wait_for(
193+
anext(wrapper),
194+
timeout=run_context.context.tool_call_timeout,
195+
)
196+
if resp is not None:
197+
if isinstance(resp, mcp.types.CallToolResult):
198+
yield resp
199+
else:
200+
text_content = mcp.types.TextContent(
201+
type="text",
202+
text=str(resp),
203+
)
204+
yield mcp.types.CallToolResult(content=[text_content])
192205
else:
193-
text_content = mcp.types.TextContent(
194-
type="text",
195-
text=str(resp),
196-
)
197-
yield mcp.types.CallToolResult(content=[text_content])
198-
else:
199-
# NOTE: Tool 在这里直接请求发送消息给用户
200-
# TODO: 是否需要判断 event.get_result() 是否为空?
201-
# 如果为空,则说明没有发送消息给用户,并且返回值为空,将返回一个特殊的 TextContent,其内容如"工具没有返回内容"
202-
yield None
206+
# NOTE: Tool 在这里直接请求发送消息给用户
207+
# TODO: 是否需要判断 event.get_result() 是否为空?
208+
# 如果为空,则说明没有发送消息给用户,并且返回值为空,将返回一个特殊的 TextContent,其内容如"工具没有返回内容"
209+
yield None
210+
except asyncio.TimeoutError:
211+
raise Exception(
212+
f"tool {tool.name} execution timeout after {run_context.context.tool_call_timeout} seconds."
213+
)
214+
except StopAsyncIteration:
215+
break
203216

204217
@classmethod
205218
async def _execute_mcp(
@@ -217,6 +230,9 @@ async def _execute_mcp(
217230
res = await session.call_tool(
218231
name=tool.name,
219232
arguments=tool_args,
233+
read_timeout_seconds=timedelta(
234+
seconds=run_context.context.tool_call_timeout
235+
),
220236
)
221237
if not res:
222238
return
@@ -307,6 +323,7 @@ async def initialize(self, ctx: PipelineContext) -> None:
307323
)
308324
self.streaming_response: bool = settings["streaming_response"]
309325
self.max_step: int = settings.get("max_agent_step", 30)
326+
self.tool_call_timeout: int = settings.get("tool_call_timeout", 60)
310327
if isinstance(self.max_step, bool): # workaround: #2622
311328
self.max_step = 30
312329
self.show_tool_use: bool = settings.get("show_tool_use_status", True)
@@ -473,6 +490,7 @@ async def process(
473490
first_provider_request=req,
474491
curr_provider_request=req,
475492
streaming=self.streaming_response,
493+
tool_call_timeout=self.tool_call_timeout,
476494
)
477495
await agent_runner.reset(
478496
provider=provider,

astrbot/dashboard/routes/tools.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,20 @@ async def test_mcp_connection(self):
273273
server_data = await request.json
274274
config = server_data.get("mcp_server_config", None)
275275

276+
if not isinstance(config, dict) or not config:
277+
return Response().error("无效的 MCP 服务器配置").__dict__
278+
279+
if "mcpServers" in config:
280+
keys = list(config["mcpServers"].keys())
281+
if not keys:
282+
return Response().error("MCP 服务器配置不能为空").__dict__
283+
if len(keys) > 1:
284+
return Response().error("一次只能配置一个 MCP 服务器配置").__dict__
285+
config = config["mcpServers"][keys[0]]
286+
else:
287+
if not config:
288+
return Response().error("MCP 服务器配置不能为空").__dict__
289+
276290
tools_name = await self.tool_mgr.test_mcp_server_connection(config)
277291
return (
278292
Response().ok(data=tools_name, message="🎉 MCP 服务器可用!").__dict__

dashboard/src/i18n/locales/en-US/features/tool-use.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@
8080
"save": "Save",
8181
"testConnection": "Test Connection",
8282
"sync": "Sync"
83+
},
84+
"tips": {
85+
"timeoutConfig": "Please configure tool call timeout separately in the configuration page"
8386
}
8487
},
8588
"serverDetail": {

dashboard/src/i18n/locales/zh-CN/features/tool-use.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@
8080
"save": "保存",
8181
"testConnection": "测试连接",
8282
"sync": "同步"
83+
},
84+
"tips": {
85+
"timeoutConfig": "工具调用的超时时间请前往配置页面单独配置"
8386
}
8487
},
8588
"serverDetail": {

dashboard/src/views/ToolUsePage.vue

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@
141141
</v-btn>
142142
</div>
143143

144+
<small style="color: grey">*{{ tm('dialogs.addServer.tips.timeoutConfig') }}</small>
145+
144146
<div class="monaco-container" style="margin-top: 16px;">
145147
<VueMonacoEditor v-model:value="serverConfigJson" theme="vs-dark" language="json" :options="{
146148
minimap: {
@@ -524,14 +526,16 @@ export default {
524526
transport: "streamable_http",
525527
url: "your mcp server url",
526528
headers: {},
527-
timeout: 30,
529+
timeout: 5,
530+
sse_read_timeout: 300,
528531
};
529532
} else if (type === 'sse') {
530533
template = {
531534
transport: "sse",
532535
url: "your mcp server url",
533536
headers: {},
534-
timeout: 30,
537+
timeout: 5,
538+
sse_read_timeout: 300,
535539
};
536540
} else {
537541
template = {

0 commit comments

Comments
 (0)