216 lines
7.4 KiB
Python
216 lines
7.4 KiB
Python
"""Basic tests for MCP Browser."""
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
import asyncio
|
|
import json
|
|
from unittest.mock import Mock, AsyncMock, patch
|
|
|
|
from mcp_browser import MCPBrowser
|
|
from mcp_browser.registry import ToolRegistry
|
|
from mcp_browser.filter import MessageFilter, VirtualToolHandler
|
|
|
|
|
|
class TestToolRegistry:
|
|
"""Test the tool registry functionality."""
|
|
|
|
def test_update_tools(self):
|
|
"""Test updating registry with tools."""
|
|
registry = ToolRegistry()
|
|
tools = [
|
|
{"name": "tool1", "description": "Tool 1"},
|
|
{"name": "tool2", "description": "Tool 2"}
|
|
]
|
|
|
|
registry.update_tools(tools)
|
|
|
|
assert len(registry.tools) == 2
|
|
assert registry.get_tool("tool1")["description"] == "Tool 1"
|
|
assert registry.get_all_tool_names() == ["tool1", "tool2"]
|
|
|
|
def test_discover_jsonpath(self):
|
|
"""Test JSONPath discovery."""
|
|
registry = ToolRegistry()
|
|
tools = [
|
|
{"name": "Bash", "description": "Run commands"},
|
|
{"name": "Read", "description": "Read files"}
|
|
]
|
|
registry.update_tools(tools)
|
|
|
|
# Test various JSONPath queries
|
|
assert registry.discover("$.tools[*].name") == ["Bash", "Read"]
|
|
assert registry.discover("$.tools[0].name") == "Bash"
|
|
assert registry.discover("$.tools[0].description") == "Run commands"
|
|
assert registry.discover("$.tools[1].name") == "Read"
|
|
assert registry.discover("$.nonexistent") is None
|
|
|
|
def test_sparse_tools(self):
|
|
"""Test sparse tool generation."""
|
|
registry = ToolRegistry()
|
|
registry.update_tools([{"name": "tool1"}, {"name": "tool2"}])
|
|
|
|
sparse = registry.get_sparse_tools()
|
|
assert len(sparse) == 3
|
|
assert sparse[0]["name"] == "mcp_discover"
|
|
assert sparse[1]["name"] == "mcp_call"
|
|
assert sparse[2]["name"] == "onboarding"
|
|
assert "2 hidden tools" in sparse[0]["description"]
|
|
|
|
|
|
class TestMessageFilter:
|
|
"""Test message filtering."""
|
|
|
|
def test_sparse_mode_filtering(self):
|
|
"""Test that tools/list responses are filtered in sparse mode."""
|
|
registry = ToolRegistry()
|
|
filter = MessageFilter(registry, sparse_mode=True)
|
|
|
|
# Mock tools/list response
|
|
message = {
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"result": {
|
|
"tools": [
|
|
{"name": "tool1", "description": "Tool 1"},
|
|
{"name": "tool2", "description": "Tool 2"}
|
|
]
|
|
}
|
|
}
|
|
|
|
filtered = filter.filter_incoming(message)
|
|
|
|
# Should replace with sparse tools
|
|
assert len(filtered["result"]["tools"]) == 3
|
|
assert filtered["result"]["tools"][0]["name"] == "mcp_discover"
|
|
assert filtered["result"]["tools"][1]["name"] == "mcp_call"
|
|
assert filtered["result"]["tools"][2]["name"] == "onboarding"
|
|
|
|
# Registry should have full tools
|
|
assert len(registry.tools) == 2
|
|
|
|
def test_duplicate_error_filtering(self):
|
|
"""Test filtering of duplicate errors for handled requests."""
|
|
registry = ToolRegistry()
|
|
filter = MessageFilter(registry)
|
|
|
|
# Mark a request as handled
|
|
filter.mark_handled(123)
|
|
|
|
# Duplicate error should be filtered
|
|
error_msg = {
|
|
"jsonrpc": "2.0",
|
|
"id": 123,
|
|
"error": {"code": -32603, "message": "Tool not found"}
|
|
}
|
|
|
|
assert filter.filter_incoming(error_msg) is None
|
|
|
|
# ID should be removed from handled set
|
|
assert 123 not in filter._handled_ids
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
class TestMCPBrowser:
|
|
"""Test the main MCP Browser functionality."""
|
|
|
|
async def test_browser_without_servers(self):
|
|
"""Test browser without any servers (unit test mode)."""
|
|
# Create browser without any servers
|
|
browser = MCPBrowser(enable_builtin_servers=False)
|
|
|
|
# Manually set up minimal config to avoid file loading
|
|
from mcp_browser.config import MCPBrowserConfig
|
|
browser.config = MCPBrowserConfig(
|
|
servers={},
|
|
default_server=None,
|
|
sparse_mode=True,
|
|
debug=False
|
|
)
|
|
|
|
# Initialize components without servers
|
|
browser.registry = ToolRegistry()
|
|
browser.filter = MessageFilter(browser.registry, sparse_mode=True)
|
|
browser.virtual_handler = VirtualToolHandler(browser.registry, browser._forward_to_server)
|
|
browser._initialized = True
|
|
|
|
# Test basic functionality
|
|
assert browser._initialized
|
|
assert browser.registry is not None
|
|
assert browser.filter is not None
|
|
|
|
# Test registry with some tools
|
|
browser.registry.update_tools([
|
|
{"name": "test1", "description": "Test tool 1"},
|
|
{"name": "test2", "description": "Test tool 2"}
|
|
])
|
|
|
|
# Test discover method
|
|
tool_names = browser.discover("$.tools[*].name")
|
|
assert tool_names == ["test1", "test2"]
|
|
|
|
# Test sparse tools
|
|
sparse = browser.registry.get_sparse_tools()
|
|
assert len(sparse) == 3
|
|
assert sparse[0]["name"] == "mcp_discover"
|
|
|
|
async def test_virtual_tool_handling(self):
|
|
"""Test virtual tool handling without real servers."""
|
|
browser = MCPBrowser(enable_builtin_servers=False)
|
|
|
|
# Set up minimal config
|
|
from mcp_browser.config import MCPBrowserConfig
|
|
browser.config = MCPBrowserConfig(
|
|
servers={},
|
|
default_server=None,
|
|
sparse_mode=True,
|
|
debug=False
|
|
)
|
|
|
|
browser.registry = ToolRegistry()
|
|
browser.filter = MessageFilter(browser.registry, sparse_mode=True)
|
|
browser.virtual_handler = VirtualToolHandler(browser.registry, browser._forward_to_server)
|
|
browser._initialized = True
|
|
|
|
# Add some test tools
|
|
browser.registry.update_tools([
|
|
{"name": "tool1", "description": "First tool"},
|
|
{"name": "tool2", "description": "Second tool"}
|
|
])
|
|
|
|
# Test mcp_discover virtual tool
|
|
response = await browser.virtual_handler.handle_tool_call({
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "tools/call",
|
|
"params": {
|
|
"name": "mcp_discover",
|
|
"arguments": {"jsonpath": "$.tools[*].name"}
|
|
}
|
|
})
|
|
|
|
assert response is not None
|
|
assert "result" in response
|
|
assert "content" in response["result"]
|
|
|
|
# Parse the response - it returns the actual values, not full objects
|
|
content = json.loads(response["result"]["content"][0]["text"])
|
|
assert content == ["tool1", "tool2"]
|
|
|
|
# Also test getting full tools
|
|
response2 = await browser.virtual_handler.handle_tool_call({
|
|
"jsonrpc": "2.0",
|
|
"id": 2,
|
|
"method": "tools/call",
|
|
"params": {
|
|
"name": "mcp_discover",
|
|
"arguments": {"jsonpath": "$.tools[0]"}
|
|
}
|
|
})
|
|
|
|
content2 = json.loads(response2["result"]["content"][0]["text"])
|
|
assert content2["name"] == "tool1"
|
|
assert content2["description"] == "First tool"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"]) |