From 3e97ed903c31a7f8111cc8c20eb116b009afc7b9 Mon Sep 17 00:00:00 2001 From: oudecheng <13802883547@139.com> Date: Tue, 11 Aug 2026 21:53:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(mcp):=20=E4=B8=BA=20MCP=20=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E8=B0=83=E7=94=A8=E5=A2=9E=E5=8A=A0=E8=B6=85=E6=97=B6?= =?UTF-8?q?=E4=BF=9D=E6=8A=A4=EF=BC=8C=E9=BB=98=E8=AE=A4=205=20=E5=88=86?= =?UTF-8?q?=E9=92=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 McpToolWrapper 适配层用 tokio::time::timeout 包裹 call_tool,防止外部 MCP server 挂起导致 agent loop 无限阻塞。超时时间通过 config.mcp_tool_timeout_secs 配置(默认 300 秒,0=不超时),前端 McpTab 设置页提供输入框。 --- src/cli/init.rs | 2 + src/config/mod.rs | 7 +++ src/gateway/mod.rs | 1 + src/gateway/runtime.rs | 4 ++ src/gateway/session.rs | 1 + src/mcp/client.rs | 3 +- src/mcp/tool_adapter.rs | 51 ++++++++++++++++++--- web/src/components/Settings/tabs/McpTab.tsx | 12 +++++ web/src/components/Settings/types.ts | 1 + 9 files changed, 74 insertions(+), 8 deletions(-) diff --git a/src/cli/init.rs b/src/cli/init.rs index 8a1a491..b2b1563 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -78,6 +78,7 @@ impl InitWizard { tools: crate::config::ToolsConfig::default(), memory_maintenance: crate::config::MemoryMaintenanceConfig::default(), mcp_servers: HashMap::new(), + mcp_tool_timeout_secs: 300, image_context: crate::config::ImageContextConfig::default(), subagents: crate::config::SubagentsConfig::default(), experts: crate::config::ExpertsConfig::default(), @@ -843,6 +844,7 @@ impl InitWizard { tools: existing.tools.clone(), memory_maintenance: existing.memory_maintenance.clone(), mcp_servers: existing.mcp_servers.clone(), + mcp_tool_timeout_secs: existing.mcp_tool_timeout_secs, image_context: existing.image_context.clone(), subagents: existing.subagents.clone(), experts: existing.experts.clone(), diff --git a/src/config/mod.rs b/src/config/mod.rs index db8b504..f7564e9 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -34,6 +34,9 @@ pub struct Config { pub memory_maintenance: MemoryMaintenanceConfig, #[serde(default, rename = "mcpServers")] pub mcp_servers: HashMap, + /// MCP 工具调用超时时间(秒)。0 表示不超时。默认 300(5 分钟)。 + #[serde(default = "default_mcp_tool_timeout_secs")] + pub mcp_tool_timeout_secs: u64, #[serde(default)] pub image_context: ImageContextConfig, #[serde(default)] @@ -556,6 +559,10 @@ fn default_max_retries() -> u32 { 3 } +fn default_mcp_tool_timeout_secs() -> u64 { + 300 +} + #[derive(Debug, Clone, Deserialize, Serialize)] pub struct GatewayConfig { #[serde(default = "default_gateway_host")] diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 35257fd..e3cbd6d 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -118,6 +118,7 @@ impl GatewayState { config.memory_maintenance.clone(), session_ttl_hours, mcp_config, + config.mcp_tool_timeout_secs, Some(bus.clone()), Arc::new(crate::config::ModelResolver::from_config(&config)), config.compaction.clone(), diff --git a/src/gateway/runtime.rs b/src/gateway/runtime.rs index 093100f..5165c7c 100644 --- a/src/gateway/runtime.rs +++ b/src/gateway/runtime.rs @@ -56,6 +56,7 @@ pub(crate) fn build_session_manager( maintenance_config: MemoryMaintenanceConfig, session_ttl_hours: Option, mcp_config: crate::mcp::McpConfig, + mcp_tool_timeout_secs: u64, bus: Option>, model_resolver: Arc, compaction_config: CompactionConfig, @@ -84,6 +85,7 @@ pub(crate) fn build_session_manager( maintenance_config, session_ttl_hours, mcp_config, + mcp_tool_timeout_secs, bus, model_resolver, compaction_config, @@ -106,6 +108,7 @@ pub(crate) fn build_session_manager_with_sender( maintenance_config: MemoryMaintenanceConfig, session_ttl_hours: Option, mcp_config: crate::mcp::McpConfig, + mcp_tool_timeout_secs: u64, bus: Option>, model_resolver: Arc, compaction_config: CompactionConfig, @@ -192,6 +195,7 @@ pub(crate) fn build_session_manager_with_sender( manager.clone(), server_key.clone(), tool_info, + mcp_tool_timeout_secs, ); mcp_tools_for_subagents.push(wrapper); } diff --git a/src/gateway/session.rs b/src/gateway/session.rs index 8ffebb1..ef2c0c9 100644 --- a/src/gateway/session.rs +++ b/src/gateway/session.rs @@ -747,6 +747,7 @@ impl SessionManager { maintenance_config, session_ttl_hours, mcp_config, + 300, None, model_resolver, crate::config::CompactionConfig::default(), diff --git a/src/mcp/client.rs b/src/mcp/client.rs index 9836e36..e800f93 100644 --- a/src/mcp/client.rs +++ b/src/mcp/client.rs @@ -788,13 +788,14 @@ impl McpInitializer { pub async fn register_tools( &mut self, registry: &mut crate::tools::ToolRegistry, + timeout_secs: u64, ) -> anyhow::Result<()> { if let Some(manager) = self.manager.clone() { // Wait for connections to complete first self.wait_for_connections().await?; tracing::info!("Registering MCP tools after connections completed"); - crate::mcp::register_mcp_tools(manager, registry).await?; + crate::mcp::register_mcp_tools(manager, registry, timeout_secs).await?; } Ok(()) } diff --git a/src/mcp/tool_adapter.rs b/src/mcp/tool_adapter.rs index 1ce1ee2..4dbf012 100644 --- a/src/mcp/tool_adapter.rs +++ b/src/mcp/tool_adapter.rs @@ -37,11 +37,18 @@ pub struct McpToolWrapper { full_name: String, /// Tool information from MCP server tool_info: Tool, + /// Tool call timeout in seconds (0 = no timeout) + timeout_secs: u64, } impl McpToolWrapper { /// Create a new tool wrapper - pub fn new(manager: Arc, server_key: String, tool_info: Tool) -> Self { + pub fn new( + manager: Arc, + server_key: String, + tool_info: Tool, + timeout_secs: u64, + ) -> Self { let tool_name = tool_info.name.clone().into_owned(); let raw_name = format!("mcp_{}_{}", server_key, tool_name); let full_name = sanitize_tool_name(&raw_name); @@ -61,6 +68,7 @@ impl McpToolWrapper { tool_name, full_name, tool_info, + timeout_secs, } } @@ -98,10 +106,33 @@ impl PicoBotTool for McpToolWrapper { "Calling MCP tool" ); - let result = self + let call = self .manager - .call_tool(&self.server_key, &self.tool_name, args) - .await?; + .call_tool(&self.server_key, &self.tool_name, args); + + let result = if self.timeout_secs > 0 { + tokio::time::timeout( + std::time::Duration::from_secs(self.timeout_secs), + call, + ) + .await + .map_err(|_| { + tracing::warn!( + server_key = %self.server_key, + tool = %self.tool_name, + timeout_secs = self.timeout_secs, + "MCP tool call timed out" + ); + anyhow::anyhow!( + "MCP tool '{}' on server '{}' timed out after {}s", + self.tool_name, + self.server_key, + self.timeout_secs + ) + })?? + } else { + call.await? + }; // Convert MCP CallToolResult to PicoBot ToolResult let output = extract_text_content(&result); @@ -147,11 +178,17 @@ fn extract_text_content(result: &rmcp::model::CallToolResult) -> String { pub async fn register_mcp_tools( manager: Arc, registry: &mut crate::tools::registry::ToolRegistry, + timeout_secs: u64, ) -> anyhow::Result<()> { let all_tools = manager.all_tools().await; for (server_key, tool_info) in all_tools { - let wrapper = McpToolWrapper::new(manager.clone(), server_key.clone(), tool_info); + let wrapper = McpToolWrapper::new( + manager.clone(), + server_key.clone(), + tool_info, + timeout_secs, + ); tracing::info!( name = %wrapper.name(), @@ -198,7 +235,7 @@ mod tests { .clone(); let tool_info = Tool::new("echo", "Echo tool", schema); - let wrapper = McpToolWrapper::new(manager, "filesystem".to_string(), tool_info); + let wrapper = McpToolWrapper::new(manager, "filesystem".to_string(), tool_info, 300); assert_eq!(wrapper.name(), "mcp_filesystem_echo"); assert_eq!(wrapper.original_name(), "echo"); assert_eq!(wrapper.server_key(), "filesystem"); @@ -216,7 +253,7 @@ mod tests { .clone(); let tool_info = Tool::new("tools.list:read", "Namespaced tool", schema); - let wrapper = McpToolWrapper::new(manager, "github.api".to_string(), tool_info); + let wrapper = McpToolWrapper::new(manager, "github.api".to_string(), tool_info, 300); // mcp_github.api_tools.list:read → mcp_github_api_tools_list_read assert_eq!(wrapper.name(), "mcp_github_api_tools_list_read"); // Original identifiers preserved for routing diff --git a/web/src/components/Settings/tabs/McpTab.tsx b/web/src/components/Settings/tabs/McpTab.tsx index 8718bf6..b6eb512 100644 --- a/web/src/components/Settings/tabs/McpTab.tsx +++ b/web/src/components/Settings/tabs/McpTab.tsx @@ -58,6 +58,18 @@ export function McpTab({ config, update, setToast }: Props) { return (
+
+ + update('mcp_tool_timeout_secs', Number(e.target.value))} + className={inputCls} + min={0} + step={30} + /> + +
{mcpStatus && mcpStatus.enabled && (
diff --git a/web/src/components/Settings/types.ts b/web/src/components/Settings/types.ts index ba24df3..3b8f7e3 100644 --- a/web/src/components/Settings/types.ts +++ b/web/src/components/Settings/types.ts @@ -257,6 +257,7 @@ export interface AppConfig { client: ClientConfig; channels: Record; mcpServers: Record; + mcp_tool_timeout_secs: number; } export type TabId =