"""Unit tests for navi/mcp/ infrastructure."""
from unittest.mock import AsyncMock
import pytest
from navi.mcp.client import McpClient
from navi.mcp.config import McpServerConfig, load_mcp_servers
from navi.mcp.manager import McpManager
from navi.mcp.tools import McpTool
class TestMcpServerConfig:
def test_stdio_config(self):
cfg = McpServerConfig(
transport="stdio",
command="python",
args=["-m", "app.mcp_server"],
env={"FOO": "bar"},
)
assert cfg.is_stdio
assert not cfg.is_sse
assert cfg.command == "python"
def test_sse_config(self):
cfg = McpServerConfig(
transport="sse",
url="http://localhost:3001/sse",
headers={"Authorization": "Bearer token"},
)
assert cfg.is_sse
assert not cfg.is_stdio
assert cfg.url == "http://localhost:3001/sse"
def test_default_transport_is_stdio(self):
cfg = McpServerConfig(command="npx")
assert cfg.transport == "stdio"
class TestLoadMcpServers:
def test_missing_file_returns_empty(self, tmp_path):
result = load_mcp_servers(tmp_path / "nonexistent.json")
assert result == {}
def test_loads_valid_json(self, tmp_path):
path = tmp_path / "mcp_servers.json"
path.write_text(
'{"book": {"transport": "stdio", "command": "python", "args": ["server.py"]}}'
)
result = load_mcp_servers(path)
assert "book" in result
assert result["book"].command == "python"
class TestMcpManager:
async def test_load_all_with_empty_config(self):
manager = McpManager()
await manager.load_all({})
assert manager.clients == {}
async def test_disconnect_all_when_empty(self):
manager = McpManager()
await manager.disconnect_all() # should not raise
async def test_get_all_tools_skips_broken_server(self):
manager = McpManager()
client = AsyncMock(spec=McpClient)
client.connected = True
client.list_tools.side_effect = RuntimeError("boom")
manager._clients = {"bad": client}
tools = await manager.get_all_tools()
assert tools == []
def test_get_instructions_returns_selected_config_overlay_when_disconnected(self, tmp_path):
path = tmp_path / "mcp_servers.json"
path.write_text(
'{"gnexus-book": {"transport": "sse", "url": "http://example/sse", "instructions": "Use book."}}'
)
manager = McpManager(config_path=path)
instructions = manager.get_instructions({"gnexus-book"})
assert instructions == {"gnexus-book": "Use book."}
def test_get_instructions_filters_to_selected_servers(self, tmp_path):
path = tmp_path / "mcp_servers.json"
path.write_text(
'{"gnexus-book": {"instructions": "Use book."}, "other": {"instructions": "Use other."}}'
)
manager = McpManager(config_path=path)
instructions = manager.get_instructions({"gnexus-book"})
assert instructions == {"gnexus-book": "Use book."}
class TestMcpTool:
def test_name_prefix(self):
mock_manager = AsyncMock(spec=McpManager)
tool = McpTool(
server_name="gnexus-book",
tool_name="search_docs",
description="Search docs",
parameters={"type": "object", "properties": {}},
manager=mock_manager,
)
assert tool.name == "mcp_gnexus-book_search_docs"
async def test_execute_success(self):
mock_manager = AsyncMock(spec=McpManager)
mock_manager.call_tool.return_value = "found 3 results"
tool = McpTool(
server_name="book",
tool_name="search",
description="",
parameters={},
manager=mock_manager,
)
result = await tool.execute({"query": "foo"})
assert result.success
assert result.output == "found 3 results"
mock_manager.call_tool.assert_awaited_once_with("book", "search", {"query": "foo"})
async def test_execute_failure(self):
mock_manager = AsyncMock(spec=McpManager)
mock_manager.call_tool.side_effect = RuntimeError("server down")
tool = McpTool(
server_name="book",
tool_name="search",
description="",
parameters={},
manager=mock_manager,
)
result = await tool.execute({})
assert not result.success
assert "server down" in result.error