From 7adbee9a7728b8cca2bfbb336d18877ef044970f Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Wed, 15 Oct 2025 12:01:24 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=B7=A5=E5=85=B7=E8=B0=83=E7=94=A8=E8=B6=85=E6=97=B6?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E5=B9=B6=E9=80=82=E9=85=8D=20ModelScope=20?= =?UTF-8?q?=E7=9A=84=20MCP=20Server=20=E9=85=8D=E7=BD=AE=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes: #2939 --- astrbot/core/agent/mcp_client.py | 24 ++++++++-- astrbot/core/astr_agent_context.py | 1 + astrbot/core/config/default.py | 9 ++++ .../process_stage/method/llm_request.py | 46 +++++++++++++------ astrbot/dashboard/routes/tools.py | 14 ++++++ .../i18n/locales/en-US/features/tool-use.json | 3 ++ .../i18n/locales/zh-CN/features/tool-use.json | 3 ++ dashboard/src/views/ToolUsePage.vue | 8 +++- 8 files changed, 88 insertions(+), 20 deletions(-) diff --git a/astrbot/core/agent/mcp_client.py b/astrbot/core/agent/mcp_client.py index c2ed246b0..d4b947e80 100644 --- a/astrbot/core/agent/mcp_client.py +++ b/astrbot/core/agent/mcp_client.py @@ -40,8 +40,17 @@ async def _quick_test_mcp_connection(config: dict) -> tuple[bool, str]: timeout = cfg.get("timeout", 10) try: + if "transport" in cfg: + transport_type = cfg["transport"] + elif "type" in cfg: + transport_type = cfg["type"] + else: + raise Exception("MCP 连接配置缺少 transport 或 type 字段") + async with aiohttp.ClientSession() as session: - if cfg.get("transport") == "streamable_http": + + + if transport_type == "streamable_http": test_payload = { "jsonrpc": "2.0", "method": "initialize", @@ -121,7 +130,14 @@ def logging_callback(msg: str): if not success: raise Exception(error_msg) - if cfg.get("transport") != "streamable_http": + if "transport" in cfg: + transport_type = cfg["transport"] + elif "type" in cfg: + transport_type = cfg["type"] + else: + raise Exception("MCP 连接配置缺少 transport 或 type 字段") + + if transport_type != "streamable_http": # SSE transport method self._streams_context = sse_client( url=cfg["url"], @@ -134,7 +150,7 @@ def logging_callback(msg: str): ) # Create a new client session - read_timeout = timedelta(seconds=cfg.get("session_read_timeout", 20)) + read_timeout = timedelta(seconds=cfg.get("session_read_timeout", 60)) self.session = await self.exit_stack.enter_async_context( mcp.ClientSession( *streams, @@ -159,7 +175,7 @@ def logging_callback(msg: str): ) # Create a new client session - read_timeout = timedelta(seconds=cfg.get("session_read_timeout", 20)) + read_timeout = timedelta(seconds=cfg.get("session_read_timeout", 60)) self.session = await self.exit_stack.enter_async_context( mcp.ClientSession( read_stream=read_s, diff --git a/astrbot/core/astr_agent_context.py b/astrbot/core/astr_agent_context.py index b09d03b3c..008c3a435 100644 --- a/astrbot/core/astr_agent_context.py +++ b/astrbot/core/astr_agent_context.py @@ -9,3 +9,4 @@ class AstrAgentContext: first_provider_request: ProviderRequest curr_provider_request: ProviderRequest streaming: bool + tool_call_timeout: int = 60 # Default tool call timeout in seconds diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 0355c51de..0841673f6 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -72,6 +72,7 @@ "show_tool_use_status": False, "streaming_segmented": False, "max_agent_step": 30, + "tool_call_timeout": 60, }, "provider_stt_settings": { "enable": False, @@ -1873,6 +1874,10 @@ "description": "工具调用轮数上限", "type": "int", }, + "tool_call_timeout": { + "description": "工具调用超时时间(秒)", + "type": "int", + }, }, }, "provider_stt_settings": { @@ -2145,6 +2150,10 @@ "description": "工具调用轮数上限", "type": "int", }, + "provider_settings.tool_call_timeout": { + "description": "工具调用超时时间(秒)", + "type": "int", + }, "provider_settings.streaming_response": { "description": "流式回复", "type": "bool", diff --git a/astrbot/core/pipeline/process_stage/method/llm_request.py b/astrbot/core/pipeline/process_stage/method/llm_request.py index e3ede6c55..6cb59674b 100644 --- a/astrbot/core/pipeline/process_stage/method/llm_request.py +++ b/astrbot/core/pipeline/process_stage/method/llm_request.py @@ -6,6 +6,7 @@ import copy import json import traceback +from datetime import timedelta from typing import AsyncGenerator, Union from astrbot.core.conversation_mgr import Conversation from astrbot.core import logger @@ -185,21 +186,33 @@ async def _execute_local( handler=awaitable, **tool_args, ) - async for resp in wrapper: - if resp is not None: - if isinstance(resp, mcp.types.CallToolResult): - yield resp + # async for resp in wrapper: + while True: + try: + resp = await asyncio.wait_for( + anext(wrapper), + timeout=run_context.context.tool_call_timeout, + ) + if resp is not None: + if isinstance(resp, mcp.types.CallToolResult): + yield resp + else: + text_content = mcp.types.TextContent( + type="text", + text=str(resp), + ) + yield mcp.types.CallToolResult(content=[text_content]) else: - text_content = mcp.types.TextContent( - type="text", - text=str(resp), - ) - yield mcp.types.CallToolResult(content=[text_content]) - else: - # NOTE: Tool 在这里直接请求发送消息给用户 - # TODO: 是否需要判断 event.get_result() 是否为空? - # 如果为空,则说明没有发送消息给用户,并且返回值为空,将返回一个特殊的 TextContent,其内容如"工具没有返回内容" - yield None + # NOTE: Tool 在这里直接请求发送消息给用户 + # TODO: 是否需要判断 event.get_result() 是否为空? + # 如果为空,则说明没有发送消息给用户,并且返回值为空,将返回一个特殊的 TextContent,其内容如"工具没有返回内容" + yield None + except asyncio.TimeoutError: + raise Exception( + f"tool {tool.name} execution timeout after {run_context.context.tool_call_timeout} seconds." + ) + except StopAsyncIteration: + break @classmethod async def _execute_mcp( @@ -217,6 +230,9 @@ async def _execute_mcp( res = await session.call_tool( name=tool.name, arguments=tool_args, + read_timeout_seconds=timedelta( + seconds=run_context.context.tool_call_timeout + ), ) if not res: return @@ -307,6 +323,7 @@ async def initialize(self, ctx: PipelineContext) -> None: ) self.streaming_response: bool = settings["streaming_response"] self.max_step: int = settings.get("max_agent_step", 30) + self.tool_call_timeout: int = settings.get("tool_call_timeout", 60) if isinstance(self.max_step, bool): # workaround: #2622 self.max_step = 30 self.show_tool_use: bool = settings.get("show_tool_use_status", True) @@ -473,6 +490,7 @@ async def process( first_provider_request=req, curr_provider_request=req, streaming=self.streaming_response, + tool_call_timeout=self.tool_call_timeout, ) await agent_runner.reset( provider=provider, diff --git a/astrbot/dashboard/routes/tools.py b/astrbot/dashboard/routes/tools.py index 1f33136ed..8fd89919a 100644 --- a/astrbot/dashboard/routes/tools.py +++ b/astrbot/dashboard/routes/tools.py @@ -273,6 +273,20 @@ async def test_mcp_connection(self): server_data = await request.json config = server_data.get("mcp_server_config", None) + if not isinstance(config, dict) or not config: + return Response().error("无效的 MCP 服务器配置").__dict__ + + if "mcpServers" in config: + keys = list(config["mcpServers"].keys()) + if not keys: + return Response().error("MCP 服务器配置不能为空").__dict__ + if len(keys) > 1: + return Response().error("一次只能配置一个 MCP 服务器配置").__dict__ + config = config["mcpServers"][keys[0]] + else: + if not config: + return Response().error("MCP 服务器配置不能为空").__dict__ + tools_name = await self.tool_mgr.test_mcp_server_connection(config) return ( Response().ok(data=tools_name, message="🎉 MCP 服务器可用!").__dict__ diff --git a/dashboard/src/i18n/locales/en-US/features/tool-use.json b/dashboard/src/i18n/locales/en-US/features/tool-use.json index 4f66d564e..2887d78fa 100644 --- a/dashboard/src/i18n/locales/en-US/features/tool-use.json +++ b/dashboard/src/i18n/locales/en-US/features/tool-use.json @@ -80,6 +80,9 @@ "save": "Save", "testConnection": "Test Connection", "sync": "Sync" + }, + "tips": { + "timeoutConfig": "Please configure tool call timeout separately in the configuration page" } }, "serverDetail": { diff --git a/dashboard/src/i18n/locales/zh-CN/features/tool-use.json b/dashboard/src/i18n/locales/zh-CN/features/tool-use.json index dfd77f0a1..c9b902c02 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/tool-use.json +++ b/dashboard/src/i18n/locales/zh-CN/features/tool-use.json @@ -80,6 +80,9 @@ "save": "保存", "testConnection": "测试连接", "sync": "同步" + }, + "tips": { + "timeoutConfig": "工具调用的超时时间请前往配置页面单独配置" } }, "serverDetail": { diff --git a/dashboard/src/views/ToolUsePage.vue b/dashboard/src/views/ToolUsePage.vue index 4cb2183da..db8fee905 100644 --- a/dashboard/src/views/ToolUsePage.vue +++ b/dashboard/src/views/ToolUsePage.vue @@ -141,6 +141,8 @@ + *{{ tm('dialogs.addServer.tips.timeoutConfig') }} +