Skip to main content
Glama
test_integration.py8.01 kB
"""Integration tests for Scout MCP server.""" from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch import pytest import scout_mcp.services.state as state_module from scout_mcp.resources import scout_resource from scout_mcp.tools import scout @pytest.fixture(autouse=True) def reset_globals() -> None: """Reset global state before each test.""" state_module._config = None state_module._pool = None @pytest.fixture def mock_ssh_config(tmp_path: Path) -> Path: """Create a temporary SSH config.""" config_file = tmp_path / "ssh_config" config_file.write_text(""" Host testhost HostName 192.168.1.100 User testuser Port 22 """) return config_file @pytest.mark.asyncio async def test_scout_hosts_lists_available(mock_ssh_config: Path) -> None: """scout('hosts') lists available SSH hosts.""" from scout_mcp.config import Config state_module._config = Config(ssh_config_path=mock_ssh_config) result = await scout("hosts") assert "testhost" in result assert "testuser@192.168.1.100" in result @pytest.mark.asyncio async def test_scout_unknown_host_returns_error() -> None: """scout with unknown host returns helpful error.""" from scout_mcp.config import Config state_module._config = Config(ssh_config_path=Path("/nonexistent")) result = await scout("unknownhost:/path") assert "Error" in result assert "Unknown host" in result @pytest.mark.asyncio async def test_scout_invalid_target_returns_error() -> None: """scout with invalid target returns error.""" result = await scout("invalid-no-colon") assert "Error" in result assert "Invalid target" in result @pytest.mark.asyncio async def test_scout_cat_file(mock_ssh_config: Path) -> None: """scout with file path cats the file.""" from scout_mcp.config import Config state_module._config = Config(ssh_config_path=mock_ssh_config) mock_conn = AsyncMock() mock_conn.is_closed = False # stat returns file mock_conn.run.side_effect = [ MagicMock(stdout="regular file", returncode=0), # stat MagicMock(stdout="file contents here", returncode=0), # cat ] with patch("asyncssh.connect", new_callable=AsyncMock) as mock_connect: mock_connect.return_value = mock_conn result = await scout("testhost:/etc/hosts") assert result == "file contents here" @pytest.mark.asyncio async def test_scout_ls_directory(mock_ssh_config: Path) -> None: """scout with directory path lists contents.""" from scout_mcp.config import Config state_module._config = Config(ssh_config_path=mock_ssh_config) mock_conn = AsyncMock() mock_conn.is_closed = False # stat returns directory mock_conn.run.side_effect = [ MagicMock(stdout="directory", returncode=0), # stat MagicMock(stdout="file1.txt\nfile2.txt", returncode=0), # ls ] with patch("asyncssh.connect", new_callable=AsyncMock) as mock_connect: mock_connect.return_value = mock_conn result = await scout("testhost:/var/log") assert "file1.txt" in result assert "file2.txt" in result @pytest.mark.asyncio async def test_scout_run_command(mock_ssh_config: Path) -> None: """scout with query runs the command.""" from scout_mcp.config import Config state_module._config = Config(ssh_config_path=mock_ssh_config) mock_conn = AsyncMock() mock_conn.is_closed = False mock_conn.run.return_value = MagicMock( stdout="TODO: fix this", stderr="", returncode=0 ) with patch("asyncssh.connect", new_callable=AsyncMock) as mock_connect: mock_connect.return_value = mock_conn result = await scout("testhost:~/code", "rg 'TODO'") assert "TODO: fix this" in result def test_hosts_resource_exists() -> None: """Verify hosts resource is registered.""" from scout_mcp.server import mcp # Check resource is registered (FastMCP stores resources differently) assert hasattr(mcp, "resource") @pytest.mark.asyncio async def test_scout_resource_template_exists() -> None: """Verify scout resource template is registered.""" from scout_mcp.server import mcp # Check that we have a resource template registered templates = await mcp.get_resource_templates() assert "scout://{host}/{path*}" in templates @pytest.mark.asyncio async def test_scout_resource_reads_file(mock_ssh_config: Path) -> None: """scout resource reads file contents.""" from scout_mcp.config import Config state_module._config = Config(ssh_config_path=mock_ssh_config) mock_conn = AsyncMock() mock_conn.is_closed = False # stat returns file, then cat mock_conn.run.side_effect = [ MagicMock(stdout="regular file", returncode=0), # stat MagicMock(stdout="file contents from resource", returncode=0), # cat ] with patch("asyncssh.connect", new_callable=AsyncMock) as mock_connect: mock_connect.return_value = mock_conn result = await scout_resource("testhost", "etc/hosts") assert result == "file contents from resource" @pytest.mark.asyncio async def test_scout_resource_lists_directory(mock_ssh_config: Path) -> None: """scout resource lists directory contents.""" from scout_mcp.config import Config state_module._config = Config(ssh_config_path=mock_ssh_config) mock_conn = AsyncMock() mock_conn.is_closed = False # stat returns directory, then ls mock_conn.run.side_effect = [ MagicMock(stdout="directory", returncode=0), # stat MagicMock(stdout="drwxr-xr-x 2 root root 4096 nginx", returncode=0), # ls ] with patch("asyncssh.connect", new_callable=AsyncMock) as mock_connect: mock_connect.return_value = mock_conn result = await scout_resource("testhost", "etc/nginx") assert "nginx" in result @pytest.mark.asyncio async def test_scout_resource_unknown_host_raises() -> None: """scout resource raises ResourceError for unknown host.""" from fastmcp.exceptions import ResourceError from scout_mcp.config import Config state_module._config = Config(ssh_config_path=Path("/nonexistent")) with pytest.raises(ResourceError, match="Unknown host"): await scout_resource("unknownhost", "etc/hosts") @pytest.mark.asyncio async def test_scout_resource_path_not_found_raises(mock_ssh_config: Path) -> None: """scout resource raises ResourceError for missing path.""" from fastmcp.exceptions import ResourceError from scout_mcp.config import Config state_module._config = Config(ssh_config_path=mock_ssh_config) mock_conn = AsyncMock() mock_conn.is_closed = False # stat returns empty (path not found) mock_conn.run.return_value = MagicMock(stdout="", returncode=1) with patch("asyncssh.connect", new_callable=AsyncMock) as mock_connect: mock_connect.return_value = mock_conn with pytest.raises(ResourceError, match="Path not found"): await scout_resource("testhost", "nonexistent/path") @pytest.mark.asyncio async def test_scout_resource_normalizes_path(mock_ssh_config: Path) -> None: """scout resource adds leading slash to paths.""" from scout_mcp.config import Config state_module._config = Config(ssh_config_path=mock_ssh_config) mock_conn = AsyncMock() mock_conn.is_closed = False # Capture the command to verify path normalization mock_conn.run.side_effect = [ MagicMock(stdout="regular file", returncode=0), # stat MagicMock(stdout="content", returncode=0), # cat ] with patch("asyncssh.connect", new_callable=AsyncMock) as mock_connect: mock_connect.return_value = mock_conn await scout_resource("testhost", "var/log/syslog") # Verify the stat command was called with /var/log/syslog first_call = mock_conn.run.call_args_list[0] assert "/var/log/syslog" in str(first_call)

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jmagar/scout_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server