"""Tests for the mcp.py module in serena.""" import pytest from mcp.server.fastmcp.tools.base import Tool as MCPTool from serena.agent import Tool, ToolRegistry from serena.config.context_mode import SerenaAgentContext from serena.mcp import SerenaMCPFactory make_tool = SerenaMCPFactory.make_mcp_tool # Create a mock agent for tool initialization class MockAgent: def __init__(self): self.project_config = None self.serena_config = None @staticmethod def get_context() -> SerenaAgentContext: return SerenaAgentContext.load_default() class BaseMockTool(Tool): """A mock Tool class for testing.""" def __init__(self): super().__init__(MockAgent()) class BasicTool(BaseMockTool): """A mock Tool class for testing.""" def apply(self, name: str, age: int = 0) -> str: """This is a test function. :param name: The person's name :param age: The person's age :return: A greeting message """ return f"Hello {name}, you are {age} years old!" def apply_ex( self, log_call: bool = True, catch_exceptions: bool = True, **kwargs, ) -> str: """Mock implementation of apply_ex.""" return self.apply(**kwargs) def test_make_tool_basic() -> None: """Test that make_tool correctly creates an MCP tool from a Tool object.""" mock_tool = BasicTool() mcp_tool = make_tool(mock_tool) # Test that the MCP tool has the correct properties assert isinstance(mcp_tool, MCPTool) assert mcp_tool.name == "basic" assert "This is a test function. Returns A greeting message." in mcp_tool.description # Test that the parameters were correctly processed parameters = mcp_tool.parameters assert "properties" in parameters assert "name" in parameters["properties"] assert "age" in parameters["properties"] assert parameters["properties"]["name"]["description"] == "The person's name." assert parameters["properties"]["age"]["description"] == "The person's age." def test_make_tool_execution() -> None: """Test that the execution function created by make_tool works correctly.""" mock_tool = BasicTool() mcp_tool = make_tool(mock_tool) # Execute the MCP tool function result = mcp_tool.fn(name="Alice", age=30) assert result == "Hello Alice, you are 30 years old!" def test_make_tool_no_params() -> None: """Test make_tool with a function that has no parameters.""" class NoParamsTool(BaseMockTool): def apply(self) -> str: """This is a test function with no parameters. :return: A simple result """ return "Simple result" def apply_ex(self, *args, **kwargs) -> str: return self.apply() tool = NoParamsTool() mcp_tool = make_tool(tool) assert mcp_tool.name == "no_params" assert "This is a test function with no parameters. Returns A simple result." in mcp_tool.description assert mcp_tool.parameters["properties"] == {} def test_make_tool_no_return_description() -> None: """Test make_tool with a function that has no return description.""" class NoReturnTool(BaseMockTool): def apply(self, param: str) -> str: """This is a test function. :param param: The parameter """ return f"Processed: {param}" def apply_ex(self, *args, **kwargs) -> str: return self.apply(**kwargs) tool = NoReturnTool() mcp_tool = make_tool(tool) assert mcp_tool.name == "no_return" assert mcp_tool.description == "This is a test function." assert mcp_tool.parameters["properties"]["param"]["description"] == "The parameter." def test_make_tool_parameter_not_in_docstring() -> None: """Test make_tool when a parameter in properties is not in the docstring.""" class MissingParamTool(BaseMockTool): def apply(self, name: str, missing_param: str = "") -> str: """This is a test function. :param name: The person's name """ return f"Hello {name}! Missing param: {missing_param}" def apply_ex(self, *args, **kwargs) -> str: return self.apply(**kwargs) tool = MissingParamTool() mcp_tool = make_tool(tool) assert "name" in mcp_tool.parameters["properties"] assert "missing_param" in mcp_tool.parameters["properties"] assert mcp_tool.parameters["properties"]["name"]["description"] == "The person's name." assert "description" not in mcp_tool.parameters["properties"]["missing_param"] def test_make_tool_multiline_docstring() -> None: """Test make_tool with a complex multi-line docstring.""" class ComplexDocTool(BaseMockTool): def apply(self, project_file_path: str, host: str, port: int) -> str: """Create an MCP server. This function creates and configures a Model Context Protocol server with the specified settings. :param project_file_path: The path to the project file, or None :param host: The host to bind to :param port: The port to bind to :return: A configured FastMCP server instance """ return f"Server config: {project_file_path}, {host}:{port}" def apply_ex(self, *args, **kwargs) -> str: return self.apply(**kwargs) tool = ComplexDocTool() mcp_tool = make_tool(tool) assert "Create an MCP server" in mcp_tool.description assert "Returns A configured FastMCP server instance" in mcp_tool.description assert mcp_tool.parameters["properties"]["project_file_path"]["description"] == "The path to the project file, or None." assert mcp_tool.parameters["properties"]["host"]["description"] == "The host to bind to." assert mcp_tool.parameters["properties"]["port"]["description"] == "The port to bind to." def test_make_tool_capitalization_and_periods() -> None: """Test that make_tool properly handles capitalization and periods in descriptions.""" class FormatTool(BaseMockTool): def apply(self, param1: str, param2: str, param3: str) -> str: """Test function. :param param1: lowercase description :param param2: description with period. :param param3: description with Capitalized word. """ return f"Formatted: {param1}, {param2}, {param3}" def apply_ex(self, *args, **kwargs) -> str: return self.apply(**kwargs) tool = FormatTool() mcp_tool = make_tool(tool) assert mcp_tool.parameters["properties"]["param1"]["description"] == "Lowercase description." assert mcp_tool.parameters["properties"]["param2"]["description"] == "Description with period." assert mcp_tool.parameters["properties"]["param3"]["description"] == "Description with Capitalized word." def test_make_tool_missing_apply() -> None: """Test make_tool with a tool that doesn't have an apply method.""" class BadTool(BaseMockTool): pass tool = BadTool() with pytest.raises(AttributeError): make_tool(tool) @pytest.mark.parametrize( "docstring, expected_description", [ ( """This is a test function. :param param: The parameter :return: A result """, "This is a test function. Returns A result.", ), ( """ :param param: The parameter :return: A result """, "Returns A result.", ), ( """ :param param: The parameter """, "", ), ("Description without params.", "Description without params."), ], ) def test_make_tool_descriptions(docstring, expected_description) -> None: """Test make_tool with various docstring formats.""" class TestTool(BaseMockTool): def apply(self, param: str) -> str: return f"Result: {param}" def apply_ex(self, *args, **kwargs) -> str: return self.apply(**kwargs) # Dynamically set the docstring TestTool.apply.__doc__ = docstring tool = TestTool() mcp_tool = make_tool(tool) assert mcp_tool.name == "test" assert mcp_tool.description == expected_description def is_test_mock_class(tool_class: type) -> bool: """Check if a class is a test mock class.""" # Check if the class is defined in a test module module_name = tool_class.__module__ return ( module_name.startswith(("test.", "tests.")) or "test_" in module_name or tool_class.__name__ in [ "BaseMockTool", "BasicTool", "BadTool", "NoParamsTool", "NoReturnTool", "MissingParamTool", "ComplexDocTool", "FormatTool", "NoDescriptionTool", ] ) @pytest.mark.parametrize("tool_class", ToolRegistry().get_all_tool_classes()) def test_make_tool_all_tools(tool_class) -> None: """Test that make_tool works for all tools in the codebase.""" # Create an instance of the tool tool_instance = tool_class(MockAgent()) # Try to create an MCP tool from it mcp_tool = make_tool(tool_instance) # Basic validation assert isinstance(mcp_tool, MCPTool) assert mcp_tool.name == tool_class.get_name_from_cls() # The description should be a string (either from docstring or default) assert isinstance(mcp_tool.description, str)