forked from Rockachopa/Timmy-time-dashboard
276 lines
9.1 KiB
Python
276 lines
9.1 KiB
Python
|
|
"""Functional tests for MCP Discovery and Bootstrap - tests actual behavior.
|
||
|
|
|
||
|
|
These tests verify the MCP system works end-to-end.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import asyncio
|
||
|
|
import sys
|
||
|
|
import types
|
||
|
|
from pathlib import Path
|
||
|
|
from unittest.mock import patch
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from mcp.discovery import ToolDiscovery, mcp_tool, DiscoveredTool
|
||
|
|
from mcp.bootstrap import auto_bootstrap, bootstrap_from_directory
|
||
|
|
from mcp.registry import ToolRegistry
|
||
|
|
|
||
|
|
|
||
|
|
class TestMCPToolDecoratorFunctional:
|
||
|
|
"""Functional tests for @mcp_tool decorator."""
|
||
|
|
|
||
|
|
def test_decorator_marks_function(self):
|
||
|
|
"""Test that decorator properly marks function as tool."""
|
||
|
|
@mcp_tool(name="my_tool", category="test", tags=["a", "b"])
|
||
|
|
def my_function(x: str) -> str:
|
||
|
|
"""Do something."""
|
||
|
|
return x
|
||
|
|
|
||
|
|
assert hasattr(my_function, "_mcp_tool")
|
||
|
|
assert my_function._mcp_tool is True
|
||
|
|
assert my_function._mcp_name == "my_tool"
|
||
|
|
assert my_function._mcp_category == "test"
|
||
|
|
assert my_function._mcp_tags == ["a", "b"]
|
||
|
|
assert "Do something" in my_function._mcp_description
|
||
|
|
|
||
|
|
def test_decorator_uses_defaults(self):
|
||
|
|
"""Test decorator uses sensible defaults."""
|
||
|
|
@mcp_tool()
|
||
|
|
def another_function():
|
||
|
|
pass
|
||
|
|
|
||
|
|
assert another_function._mcp_name == "another_function"
|
||
|
|
assert another_function._mcp_category == "general"
|
||
|
|
assert another_function._mcp_tags == []
|
||
|
|
|
||
|
|
|
||
|
|
class TestToolDiscoveryFunctional:
|
||
|
|
"""Functional tests for tool discovery."""
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def mock_module(self):
|
||
|
|
"""Create a mock module with tools."""
|
||
|
|
module = types.ModuleType("test_discovery_module")
|
||
|
|
module.__file__ = "test_discovery_module.py"
|
||
|
|
|
||
|
|
@mcp_tool(name="echo", category="test")
|
||
|
|
def echo_func(message: str) -> str:
|
||
|
|
"""Echo a message."""
|
||
|
|
return message
|
||
|
|
|
||
|
|
@mcp_tool(name="add", category="math")
|
||
|
|
def add_func(a: int, b: int) -> int:
|
||
|
|
"""Add numbers."""
|
||
|
|
return a + b
|
||
|
|
|
||
|
|
def not_a_tool():
|
||
|
|
"""Not decorated."""
|
||
|
|
pass
|
||
|
|
|
||
|
|
module.echo_func = echo_func
|
||
|
|
module.add_func = add_func
|
||
|
|
module.not_a_tool = not_a_tool
|
||
|
|
|
||
|
|
sys.modules["test_discovery_module"] = module
|
||
|
|
yield module
|
||
|
|
del sys.modules["test_discovery_module"]
|
||
|
|
|
||
|
|
def test_discover_module_finds_tools(self, mock_module):
|
||
|
|
"""Test discovering tools from a module."""
|
||
|
|
registry = ToolRegistry()
|
||
|
|
discovery = ToolDiscovery(registry=registry)
|
||
|
|
|
||
|
|
tools = discovery.discover_module("test_discovery_module")
|
||
|
|
|
||
|
|
names = [t.name for t in tools]
|
||
|
|
assert "echo" in names
|
||
|
|
assert "add" in names
|
||
|
|
assert "not_a_tool" not in names
|
||
|
|
|
||
|
|
def test_discovered_tool_has_correct_metadata(self, mock_module):
|
||
|
|
"""Test discovered tools have correct metadata."""
|
||
|
|
registry = ToolRegistry()
|
||
|
|
discovery = ToolDiscovery(registry=registry)
|
||
|
|
|
||
|
|
tools = discovery.discover_module("test_discovery_module")
|
||
|
|
|
||
|
|
echo = next(t for t in tools if t.name == "echo")
|
||
|
|
assert echo.category == "test"
|
||
|
|
assert "Echo a message" in echo.description
|
||
|
|
|
||
|
|
def test_discovered_tool_has_schema(self, mock_module):
|
||
|
|
"""Test discovered tools have generated schemas."""
|
||
|
|
registry = ToolRegistry()
|
||
|
|
discovery = ToolDiscovery(registry=registry)
|
||
|
|
|
||
|
|
tools = discovery.discover_module("test_discovery_module")
|
||
|
|
|
||
|
|
add = next(t for t in tools if t.name == "add")
|
||
|
|
assert "properties" in add.parameters_schema
|
||
|
|
assert "a" in add.parameters_schema["properties"]
|
||
|
|
assert "b" in add.parameters_schema["properties"]
|
||
|
|
|
||
|
|
def test_discover_nonexistent_module(self):
|
||
|
|
"""Test discovering from non-existent module returns empty list."""
|
||
|
|
registry = ToolRegistry()
|
||
|
|
discovery = ToolDiscovery(registry=registry)
|
||
|
|
|
||
|
|
tools = discovery.discover_module("nonexistent_xyz_module")
|
||
|
|
|
||
|
|
assert tools == []
|
||
|
|
|
||
|
|
|
||
|
|
class TestToolRegistrationFunctional:
|
||
|
|
"""Functional tests for tool registration via discovery."""
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def mock_module(self):
|
||
|
|
"""Create a mock module with tools."""
|
||
|
|
module = types.ModuleType("test_register_module")
|
||
|
|
module.__file__ = "test_register_module.py"
|
||
|
|
|
||
|
|
@mcp_tool(name="register_test", category="test")
|
||
|
|
def test_func(value: str) -> str:
|
||
|
|
"""Test function."""
|
||
|
|
return value.upper()
|
||
|
|
|
||
|
|
module.test_func = test_func
|
||
|
|
sys.modules["test_register_module"] = module
|
||
|
|
yield module
|
||
|
|
del sys.modules["test_register_module"]
|
||
|
|
|
||
|
|
def test_auto_register_adds_to_registry(self, mock_module):
|
||
|
|
"""Test auto_register adds tools to registry."""
|
||
|
|
registry = ToolRegistry()
|
||
|
|
discovery = ToolDiscovery(registry=registry)
|
||
|
|
|
||
|
|
registered = discovery.auto_register("test_register_module")
|
||
|
|
|
||
|
|
assert "register_test" in registered
|
||
|
|
assert registry.get("register_test") is not None
|
||
|
|
|
||
|
|
def test_registered_tool_can_execute(self, mock_module):
|
||
|
|
"""Test that registered tools can be executed."""
|
||
|
|
registry = ToolRegistry()
|
||
|
|
discovery = ToolDiscovery(registry=registry)
|
||
|
|
|
||
|
|
discovery.auto_register("test_register_module")
|
||
|
|
|
||
|
|
result = asyncio.run(
|
||
|
|
registry.execute("register_test", {"value": "hello"})
|
||
|
|
)
|
||
|
|
|
||
|
|
assert result == "HELLO"
|
||
|
|
|
||
|
|
def test_registered_tool_tracks_metrics(self, mock_module):
|
||
|
|
"""Test that tool execution tracks metrics."""
|
||
|
|
registry = ToolRegistry()
|
||
|
|
discovery = ToolDiscovery(registry=registry)
|
||
|
|
|
||
|
|
discovery.auto_register("test_register_module")
|
||
|
|
|
||
|
|
# Execute multiple times
|
||
|
|
for _ in range(3):
|
||
|
|
asyncio.run(registry.execute("register_test", {"value": "test"}))
|
||
|
|
|
||
|
|
metrics = registry.get_metrics("register_test")
|
||
|
|
assert metrics["executions"] == 3
|
||
|
|
assert metrics["health"] == "healthy"
|
||
|
|
|
||
|
|
|
||
|
|
class TestMCBootstrapFunctional:
|
||
|
|
"""Functional tests for MCP bootstrap."""
|
||
|
|
|
||
|
|
def test_auto_bootstrap_empty_list(self):
|
||
|
|
"""Test auto_bootstrap with empty packages list."""
|
||
|
|
registry = ToolRegistry()
|
||
|
|
|
||
|
|
registered = auto_bootstrap(
|
||
|
|
packages=[],
|
||
|
|
registry=registry,
|
||
|
|
force=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
assert registered == []
|
||
|
|
|
||
|
|
def test_auto_bootstrap_nonexistent_package(self):
|
||
|
|
"""Test auto_bootstrap with non-existent package."""
|
||
|
|
registry = ToolRegistry()
|
||
|
|
|
||
|
|
registered = auto_bootstrap(
|
||
|
|
packages=["nonexistent_package_12345"],
|
||
|
|
registry=registry,
|
||
|
|
force=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
assert registered == []
|
||
|
|
|
||
|
|
def test_bootstrap_status(self):
|
||
|
|
"""Test get_bootstrap_status returns expected structure."""
|
||
|
|
from mcp.bootstrap import get_bootstrap_status
|
||
|
|
|
||
|
|
status = get_bootstrap_status()
|
||
|
|
|
||
|
|
assert "auto_bootstrap_enabled" in status
|
||
|
|
assert "discovered_tools_count" in status
|
||
|
|
assert "registered_tools_count" in status
|
||
|
|
assert "default_packages" in status
|
||
|
|
|
||
|
|
|
||
|
|
class TestRegistryIntegration:
|
||
|
|
"""Integration tests for registry with discovery."""
|
||
|
|
|
||
|
|
def test_registry_discover_filtering(self):
|
||
|
|
"""Test registry discover method filters correctly."""
|
||
|
|
registry = ToolRegistry()
|
||
|
|
|
||
|
|
@mcp_tool(name="cat1", category="category1", tags=["tag1"])
|
||
|
|
def func1():
|
||
|
|
pass
|
||
|
|
|
||
|
|
@mcp_tool(name="cat2", category="category2", tags=["tag2"])
|
||
|
|
def func2():
|
||
|
|
pass
|
||
|
|
|
||
|
|
registry.register_tool(name="cat1", function=func1, category="category1", tags=["tag1"])
|
||
|
|
registry.register_tool(name="cat2", function=func2, category="category2", tags=["tag2"])
|
||
|
|
|
||
|
|
# Filter by category
|
||
|
|
cat1_tools = registry.discover(category="category1")
|
||
|
|
assert len(cat1_tools) == 1
|
||
|
|
assert cat1_tools[0].name == "cat1"
|
||
|
|
|
||
|
|
# Filter by tags
|
||
|
|
tag1_tools = registry.discover(tags=["tag1"])
|
||
|
|
assert len(tag1_tools) == 1
|
||
|
|
assert tag1_tools[0].name == "cat1"
|
||
|
|
|
||
|
|
def test_registry_to_dict(self):
|
||
|
|
"""Test registry export includes all fields."""
|
||
|
|
registry = ToolRegistry()
|
||
|
|
|
||
|
|
@mcp_tool(name="export_test", category="test", tags=["a"])
|
||
|
|
def export_func():
|
||
|
|
"""Test export."""
|
||
|
|
pass
|
||
|
|
|
||
|
|
registry.register_tool(
|
||
|
|
name="export_test",
|
||
|
|
function=export_func,
|
||
|
|
category="test",
|
||
|
|
tags=["a"],
|
||
|
|
source_module="test_module",
|
||
|
|
)
|
||
|
|
|
||
|
|
export = registry.to_dict()
|
||
|
|
|
||
|
|
assert export["total_tools"] == 1
|
||
|
|
assert export["auto_discovered_count"] == 1
|
||
|
|
|
||
|
|
tool = export["tools"][0]
|
||
|
|
assert tool["name"] == "export_test"
|
||
|
|
assert tool["category"] == "test"
|
||
|
|
assert tool["tags"] == ["a"]
|
||
|
|
assert tool["source_module"] == "test_module"
|
||
|
|
assert tool["auto_discovered"] is True
|