diff --git a/src/mcp/tool_adapter.rs b/src/mcp/tool_adapter.rs index 4dbf012..3943746 100644 --- a/src/mcp/tool_adapter.rs +++ b/src/mcp/tool_adapter.rs @@ -281,4 +281,49 @@ mod tests { // Empty stays empty assert_eq!(sanitize_tool_name(""), ""); } + + #[tokio::test] + async fn test_execute_returns_error_when_server_not_connected() { + // McpClientManager with no connected servers — call should fail immediately + // with "not connected" error, not hang or timeout. + let manager = Arc::new(McpClientManager::new()); + let schema: serde_json::Map = + serde_json::json!({"type": "object"}) + .as_object() + .unwrap() + .clone(); + let tool_info = Tool::new("echo", "Echo tool", schema); + let wrapper = McpToolWrapper::new(manager, "ghost".to_string(), tool_info, 300); + + let result = wrapper.execute(serde_json::json!({})).await; + assert!(result.is_err()); + let msg = result.unwrap_err().to_string(); + assert!( + msg.contains("not connected"), + "expected 'not connected' error, got: {msg}" + ); + } + + #[tokio::test] + async fn test_timeout_zero_does_not_wrap() { + // With timeout_secs = 0, the wrapper should pass through directly + // without tokio::time::timeout. The call still fails because no server + // is connected, but the error path is the non-timeout branch. + let manager = Arc::new(McpClientManager::new()); + let schema: serde_json::Map = + serde_json::json!({"type": "object"}) + .as_object() + .unwrap() + .clone(); + let tool_info = Tool::new("echo", "Echo tool", schema); + let wrapper = McpToolWrapper::new(manager, "ghost".to_string(), tool_info, 0); + + let result = wrapper.execute(serde_json::json!({})).await; + assert!(result.is_err()); + let msg = result.unwrap_err().to_string(); + assert!( + msg.contains("not connected"), + "expected 'not connected' error, got: {msg}" + ); + } } diff --git a/web/src/components/Settings/tabs/McpTab.tsx b/web/src/components/Settings/tabs/McpTab.tsx index b6eb512..6e206c1 100644 --- a/web/src/components/Settings/tabs/McpTab.tsx +++ b/web/src/components/Settings/tabs/McpTab.tsx @@ -63,7 +63,10 @@ export function McpTab({ config, update, setToast }: Props) { update('mcp_tool_timeout_secs', Number(e.target.value))} + onChange={(e) => { + const v = e.target.value; + update('mcp_tool_timeout_secs', v === '' ? 300 : Number(v)); + }} className={inputCls} min={0} step={30}