From 1cfb1eb4e17b5138946390c11372cbf82bb8bed4 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Fri, 27 Jun 2025 05:16:42 +0000 Subject: [PATCH] feat: create diagnostic tool with version and config info - Create new diagnostic MCP tool that provides version and system information - Get version from basic_memory.__version__ and __api_version__ - Include complete configuration dump from config.json - Add comprehensive system information (Python, OS, architecture) - Provide project context with error handling - Remove tool availability listing to avoid sync issues - Add comprehensive test suite with 11 test functions Co-authored-by: Drew Cain --- src/basic_memory/mcp/tools/__init__.py | 2 + src/basic_memory/mcp/tools/diagnostics.py | 128 +++++++++++ tests/mcp/test_tool_diagnostics.py | 258 ++++++++++++++++++++++ 3 files changed, 388 insertions(+) create mode 100644 src/basic_memory/mcp/tools/diagnostics.py create mode 100644 tests/mcp/test_tool_diagnostics.py diff --git a/src/basic_memory/mcp/tools/__init__.py b/src/basic_memory/mcp/tools/__init__.py index b49997ca..ae7ff5ef 100644 --- a/src/basic_memory/mcp/tools/__init__.py +++ b/src/basic_memory/mcp/tools/__init__.py @@ -7,6 +7,7 @@ all tools with the MCP server. # Import tools to register them with MCP from basic_memory.mcp.tools.delete_note import delete_note +from basic_memory.mcp.tools.diagnostics import diagnostics from basic_memory.mcp.tools.read_content import read_content from basic_memory.mcp.tools.build_context import build_context from basic_memory.mcp.tools.recent_activity import recent_activity @@ -34,6 +35,7 @@ __all__ = [ "create_project", "delete_note", "delete_project", + "diagnostics", "edit_note", "get_current_project", "list_directory", diff --git a/src/basic_memory/mcp/tools/diagnostics.py b/src/basic_memory/mcp/tools/diagnostics.py new file mode 100644 index 00000000..04966002 --- /dev/null +++ b/src/basic_memory/mcp/tools/diagnostics.py @@ -0,0 +1,128 @@ +"""Diagnostic tool for Basic Memory system information.""" + +import json +import platform +import sys +from pathlib import Path +from typing import Any, Dict, Optional + +import basic_memory +from basic_memory.config import config_manager +from basic_memory.mcp.tools.utils import build_mcp_tool, logger + + +@build_mcp_tool( + name="diagnostics", + description=""" + Provides comprehensive diagnostic information for Basic Memory including: + - Version information from package + - System details (Python, OS, architecture) + - Current configuration dump + - Project context + + This tool helps with troubleshooting installations and gathering system + information for support requests. + """, +) +async def diagnostics(project: Optional[str] = None) -> Dict[str, Any]: + """ + Get comprehensive diagnostic information for Basic Memory. + + Args: + project: Optional project name to get specific project context + + Returns: + Dictionary containing diagnostic information + """ + try: + # Version information + version_info = { + "basic_memory_version": basic_memory.__version__, + "api_version": basic_memory.__api_version__, + } + + # System information + system_info = { + "python_version": sys.version, + "python_executable": sys.executable, + "platform": platform.platform(), + "machine": platform.machine(), + "processor": platform.processor(), + "architecture": platform.architecture(), + } + + # Configuration information + try: + config = config_manager.load_config() + config_info = { + "config_file_path": str(config_manager.config_file), + "config_exists": config_manager.config_file.exists(), + "configuration": config.model_dump(), + } + except Exception as e: + logger.error(f"Error loading configuration: {e}") + config_info = { + "config_file_path": str(config_manager.config_file), + "config_exists": config_manager.config_file.exists(), + "error": f"Failed to load configuration: {str(e)}", + } + + # Project context + project_context = {} + if project: + try: + project_name, project_path = config_manager.get_project(project) + if project_name: + project_context = { + "requested_project": project, + "resolved_project_name": project_name, + "project_path": project_path, + "project_exists": Path(project_path).exists(), + } + else: + project_context = { + "requested_project": project, + "error": f"Project '{project}' not found", + } + except Exception as e: + project_context = { + "requested_project": project, + "error": f"Error accessing project: {str(e)}", + } + else: + try: + default_project = config_manager.default_project + default_path = config_manager.config.get_project_path() + project_context = { + "default_project": default_project, + "default_project_path": str(default_path), + "default_project_exists": default_path.exists(), + } + except Exception as e: + project_context = { + "error": f"Error accessing default project: {str(e)}", + } + + # Compile diagnostic information + diagnostic_info = { + "version": version_info, + "system": system_info, + "configuration": config_info, + "project_context": project_context, + } + + return diagnostic_info + + except Exception as e: + logger.error(f"Error generating diagnostics: {e}") + return { + "error": f"Failed to generate diagnostics: {str(e)}", + "troubleshooting": { + "suggestions": [ + "Check if Basic Memory is properly installed", + "Verify configuration file permissions", + "Ensure project paths are accessible", + "Check system dependencies", + ] + }, + } \ No newline at end of file diff --git a/tests/mcp/test_tool_diagnostics.py b/tests/mcp/test_tool_diagnostics.py new file mode 100644 index 00000000..1d7d5557 --- /dev/null +++ b/tests/mcp/test_tool_diagnostics.py @@ -0,0 +1,258 @@ +"""Tests for diagnostics MCP tool.""" + +import pytest +from unittest.mock import MagicMock, patch, mock_open + +from basic_memory.mcp.tools.diagnostics import diagnostics +from basic_memory.config import BasicMemoryConfig, ConfigManager + + +@pytest.mark.asyncio +async def test_diagnostics_basic_functionality(): + """Test basic diagnostics functionality.""" + # Mock config manager + mock_config = BasicMemoryConfig( + projects={"main": "/path/to/main", "test": "/path/to/test"}, + default_project="main" + ) + + with patch("basic_memory.mcp.tools.diagnostics.config_manager") as mock_manager: + mock_manager.load_config.return_value = mock_config + mock_manager.config_file = "/home/user/.basic-memory/config.json" + mock_manager.config_file.exists.return_value = True + mock_manager.default_project = "main" + mock_manager.get_project.return_value = ("main", "/path/to/main") + + with patch("pathlib.Path.exists", return_value=True): + result = await diagnostics.fn() + + assert "version" in result + assert "system" in result + assert "configuration" in result + assert "project_context" in result + + # Check version information + assert "basic_memory_version" in result["version"] + assert "api_version" in result["version"] + + # Check system information + assert "python_version" in result["system"] + assert "platform" in result["system"] + + # Check configuration information + assert "config_file_path" in result["configuration"] + assert "configuration" in result["configuration"] + + +@pytest.mark.asyncio +async def test_diagnostics_with_project_parameter(): + """Test diagnostics with specific project parameter.""" + mock_config = BasicMemoryConfig( + projects={"main": "/path/to/main", "test": "/path/to/test"}, + default_project="main" + ) + + with patch("basic_memory.mcp.tools.diagnostics.config_manager") as mock_manager: + mock_manager.load_config.return_value = mock_config + mock_manager.config_file = "/home/user/.basic-memory/config.json" + mock_manager.config_file.exists.return_value = True + mock_manager.get_project.return_value = ("test", "/path/to/test") + + with patch("pathlib.Path.exists", return_value=True): + result = await diagnostics.fn(project="test") + + assert "project_context" in result + assert result["project_context"]["requested_project"] == "test" + assert result["project_context"]["resolved_project_name"] == "test" + assert result["project_context"]["project_path"] == "/path/to/test" + + +@pytest.mark.asyncio +async def test_diagnostics_project_not_found(): + """Test diagnostics when requested project is not found.""" + mock_config = BasicMemoryConfig( + projects={"main": "/path/to/main"}, + default_project="main" + ) + + with patch("basic_memory.mcp.tools.diagnostics.config_manager") as mock_manager: + mock_manager.load_config.return_value = mock_config + mock_manager.config_file = "/home/user/.basic-memory/config.json" + mock_manager.config_file.exists.return_value = True + mock_manager.get_project.return_value = (None, None) + + result = await diagnostics.fn(project="nonexistent") + + assert "project_context" in result + assert result["project_context"]["requested_project"] == "nonexistent" + assert "error" in result["project_context"] + assert "not found" in result["project_context"]["error"] + + +@pytest.mark.asyncio +async def test_diagnostics_config_loading_error(): + """Test diagnostics when config loading fails.""" + with patch("basic_memory.mcp.tools.diagnostics.config_manager") as mock_manager: + mock_manager.load_config.side_effect = Exception("Config loading failed") + mock_manager.config_file = "/home/user/.basic-memory/config.json" + mock_manager.config_file.exists.return_value = True + + result = await diagnostics.fn() + + assert "configuration" in result + assert "error" in result["configuration"] + assert "Config loading failed" in result["configuration"]["error"] + + +@pytest.mark.asyncio +async def test_diagnostics_project_access_error(): + """Test diagnostics when project access fails.""" + mock_config = BasicMemoryConfig( + projects={"main": "/path/to/main"}, + default_project="main" + ) + + with patch("basic_memory.mcp.tools.diagnostics.config_manager") as mock_manager: + mock_manager.load_config.return_value = mock_config + mock_manager.config_file = "/home/user/.basic-memory/config.json" + mock_manager.config_file.exists.return_value = True + mock_manager.get_project.side_effect = Exception("Access denied") + + result = await diagnostics.fn(project="main") + + assert "project_context" in result + assert "error" in result["project_context"] + assert "Access denied" in result["project_context"]["error"] + + +@pytest.mark.asyncio +async def test_diagnostics_default_project_context(): + """Test diagnostics without project parameter uses default project.""" + mock_config = BasicMemoryConfig( + projects={"main": "/path/to/main"}, + default_project="main" + ) + + with patch("basic_memory.mcp.tools.diagnostics.config_manager") as mock_manager: + mock_manager.load_config.return_value = mock_config + mock_manager.config_file = "/home/user/.basic-memory/config.json" + mock_manager.config_file.exists.return_value = True + mock_manager.default_project = "main" + mock_manager.config = mock_config + + with patch("pathlib.Path.exists", return_value=True): + result = await diagnostics.fn() + + assert "project_context" in result + assert result["project_context"]["default_project"] == "main" + assert "default_project_path" in result["project_context"] + + +@pytest.mark.asyncio +async def test_diagnostics_default_project_error(): + """Test diagnostics when default project access fails.""" + mock_config = BasicMemoryConfig( + projects={"main": "/path/to/main"}, + default_project="main" + ) + + with patch("basic_memory.mcp.tools.diagnostics.config_manager") as mock_manager: + mock_manager.load_config.return_value = mock_config + mock_manager.config_file = "/home/user/.basic-memory/config.json" + mock_manager.config_file.exists.return_value = True + mock_manager.default_project = "main" + mock_manager.config.get_project_path.side_effect = Exception("Path error") + + result = await diagnostics.fn() + + assert "project_context" in result + assert "error" in result["project_context"] + assert "Path error" in result["project_context"]["error"] + + +@pytest.mark.asyncio +async def test_diagnostics_general_exception(): + """Test diagnostics handles general exceptions gracefully.""" + with patch("basic_memory.mcp.tools.diagnostics.basic_memory") as mock_basic_memory: + mock_basic_memory.__version__ = None # This should cause an error + + result = await diagnostics.fn() + + assert "error" in result + assert "troubleshooting" in result + assert "suggestions" in result["troubleshooting"] + assert len(result["troubleshooting"]["suggestions"]) > 0 + + +@pytest.mark.asyncio +async def test_diagnostics_config_file_not_exists(): + """Test diagnostics when config file doesn't exist.""" + mock_config = BasicMemoryConfig( + projects={"main": "/path/to/main"}, + default_project="main" + ) + + with patch("basic_memory.mcp.tools.diagnostics.config_manager") as mock_manager: + mock_manager.load_config.return_value = mock_config + mock_manager.config_file = "/home/user/.basic-memory/config.json" + mock_manager.config_file.exists.return_value = False + mock_manager.default_project = "main" + mock_manager.config = mock_config + + with patch("pathlib.Path.exists", return_value=True): + result = await diagnostics.fn() + + assert "configuration" in result + assert result["configuration"]["config_exists"] is False + assert "config_file_path" in result["configuration"] + + +@pytest.mark.asyncio +async def test_diagnostics_version_information(): + """Test that version information is correctly extracted.""" + with patch("basic_memory.mcp.tools.diagnostics.config_manager") as mock_manager: + mock_config = BasicMemoryConfig() + mock_manager.load_config.return_value = mock_config + mock_manager.config_file = "/home/user/.basic-memory/config.json" + mock_manager.config_file.exists.return_value = True + mock_manager.default_project = "main" + mock_manager.config = mock_config + + with patch("pathlib.Path.exists", return_value=True): + result = await diagnostics.fn() + + assert "version" in result + version_info = result["version"] + + # These should contain the actual values from basic_memory.__version__ and __api_version__ + assert "basic_memory_version" in version_info + assert "api_version" in version_info + assert isinstance(version_info["basic_memory_version"], str) + assert isinstance(version_info["api_version"], str) + + +@pytest.mark.asyncio +async def test_diagnostics_system_information(): + """Test that system information is correctly gathered.""" + with patch("basic_memory.mcp.tools.diagnostics.config_manager") as mock_manager: + mock_config = BasicMemoryConfig() + mock_manager.load_config.return_value = mock_config + mock_manager.config_file = "/home/user/.basic-memory/config.json" + mock_manager.config_file.exists.return_value = True + mock_manager.default_project = "main" + mock_manager.config = mock_config + + with patch("pathlib.Path.exists", return_value=True): + result = await diagnostics.fn() + + assert "system" in result + system_info = result["system"] + + # Check that all expected system fields are present + expected_fields = [ + "python_version", "python_executable", "platform", + "machine", "processor", "architecture" + ] + + for field in expected_fields: + assert field in system_info \ No newline at end of file