187 lines
6.2 KiB
Python
187 lines
6.2 KiB
Python
"""Basic tests for MCP Browser."""
|
|
|
|
import pytest
|
|
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
|
|
|
|
|
|
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[?(@.name=='Bash')]")[0]["name"] == "Bash"
|
|
assert registry.discover("$.tools[0].description") == "Run commands"
|
|
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) == 2
|
|
assert sparse[0]["name"] == "mcp_discover"
|
|
assert sparse[1]["name"] == "mcp_call"
|
|
assert "2 tools available" 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"]) == 2
|
|
assert filtered["result"]["tools"][0]["name"] == "mcp_discover"
|
|
assert filtered["result"]["tools"][1]["name"] == "mcp_call"
|
|
|
|
# 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_initialization(self):
|
|
"""Test browser initialization."""
|
|
with patch('mcp_browser.proxy.MCPServer') as mock_server_class:
|
|
mock_server = AsyncMock()
|
|
mock_server_class.return_value = mock_server
|
|
|
|
browser = MCPBrowser()
|
|
|
|
# Mock successful initialization
|
|
mock_server.send_request.side_effect = [
|
|
# Initialize response
|
|
{"result": {"protocolVersion": "0.1.0"}},
|
|
# Tools list response
|
|
{"result": {"tools": [{"name": "test", "description": "Test tool"}]}}
|
|
]
|
|
|
|
await browser.initialize()
|
|
|
|
assert browser._initialized
|
|
assert mock_server.start.called
|
|
|
|
async def test_call_method(self):
|
|
"""Test the generic call method."""
|
|
with patch('mcp_browser.proxy.MCPServer') as mock_server_class:
|
|
mock_server = AsyncMock()
|
|
mock_server_class.return_value = mock_server
|
|
mock_server.send_request.side_effect = [
|
|
{"result": {"protocolVersion": "0.1.0"}},
|
|
{"result": {"tools": []}}
|
|
]
|
|
|
|
browser = MCPBrowser()
|
|
await browser.initialize()
|
|
|
|
# Set up response handling
|
|
browser._response_buffer[1] = asyncio.Future()
|
|
|
|
# Simulate server response
|
|
async def simulate_response():
|
|
await asyncio.sleep(0.1)
|
|
browser._handle_server_message({
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"result": {"success": True}
|
|
})
|
|
|
|
asyncio.create_task(simulate_response())
|
|
|
|
# Make call
|
|
response = await browser.call({
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "test",
|
|
"params": {}
|
|
})
|
|
|
|
assert response["result"]["success"] is True
|
|
|
|
async def test_discover_method(self):
|
|
"""Test the discover method."""
|
|
browser = MCPBrowser()
|
|
browser.registry.update_tools([
|
|
{"name": "tool1", "description": "First tool"},
|
|
{"name": "tool2", "description": "Second tool"}
|
|
])
|
|
|
|
# Test discovery
|
|
tool_names = browser.discover("$.tools[*].name")
|
|
assert tool_names == ["tool1", "tool2"]
|
|
|
|
specific_tool = browser.discover("$.tools[?(@.name=='tool1')]")
|
|
assert specific_tool[0]["description"] == "First tool"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"]) |