From a4fd71c10bdff80f415749ff69e8eca387fa17b8 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Fri, 27 Jun 2025 03:36:08 +0000 Subject: [PATCH] feat: add comprehensive diagnostic tool for version and system information Add new MCP tool 'diagnostics' that provides: - Basic Memory version and API version information - System details (Python, OS, architecture) - Configuration summary (projects, database, sync settings) - Tool availability with categorization - Project context and status - Robust error handling with troubleshooting guidance The tool helps users troubleshoot installations, verify features, and gather system information for support requests. Files added: - src/basic_memory/mcp/tools/diagnostics.py: Main diagnostic tool implementation - tests/mcp/test_tool_diagnostics.py: Comprehensive test suite with 11 test functions Files modified: - src/basic_memory/mcp/tools/__init__.py: Added diagnostics tool to exports Resolves #187 Co-authored-by: Drew Cain --- src/basic_memory/mcp/tools/__init__.py | 2 + src/basic_memory/mcp/tools/diagnostics.py | 236 ++++++++++++++++++++++ tests/mcp/test_tool_diagnostics.py | 185 +++++++++++++++++ 3 files changed, 423 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..f02d8696 100644 --- a/src/basic_memory/mcp/tools/__init__.py +++ b/src/basic_memory/mcp/tools/__init__.py @@ -19,6 +19,7 @@ from basic_memory.mcp.tools.list_directory import list_directory from basic_memory.mcp.tools.edit_note import edit_note from basic_memory.mcp.tools.move_note import move_note from basic_memory.mcp.tools.sync_status import sync_status +from basic_memory.mcp.tools.diagnostics import diagnostics from basic_memory.mcp.tools.project_management import ( list_projects, switch_project, @@ -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..167364c7 --- /dev/null +++ b/src/basic_memory/mcp/tools/diagnostics.py @@ -0,0 +1,236 @@ +"""Diagnostic tool for Basic Memory MCP server.""" + +import platform +import sys +from typing import Optional +from pathlib import Path + +from loguru import logger + +from basic_memory.mcp.server import mcp +from basic_memory.mcp.project_session import get_active_project +import basic_memory + + +def _get_system_info() -> list[str]: + """Get system information for diagnostic output.""" + info_lines = [] + + try: + info_lines.extend([ + f"**Python Version**: {sys.version.split()[0]}", + f"**Platform**: {platform.system()} {platform.release()}", + f"**Architecture**: {platform.machine()}", + f"**Python Implementation**: {platform.python_implementation()}", + ]) + + # Add Python executable path + info_lines.append(f"**Python Executable**: {sys.executable}") + + # Add current working directory + info_lines.append(f"**Working Directory**: {Path.cwd()}") + + except Exception as e: + logger.debug(f"Could not get some system info: {e}") + info_lines.append(f"**System Info Error**: {str(e)}") + + return info_lines + + +def _get_configuration_summary() -> list[str]: + """Get configuration summary for diagnostic output.""" + config_lines = [] + + try: + from basic_memory.config import app_config, config_manager + + config_lines.extend([ + f"**Environment**: {app_config.env}", + f"**Log Level**: {app_config.log_level}", + f"**Default Project**: {app_config.default_project}", + f"**Total Projects**: {len(app_config.projects)}", + f"**Sync Delay**: {app_config.sync_delay}ms", + f"**Update Permalinks on Move**: {app_config.update_permalinks_on_move}", + f"**Sync Changes**: {app_config.sync_changes}", + ]) + + # Add database info + config_lines.append(f"**Database Path**: {app_config.app_database_path}") + config_lines.append(f"**Config Directory**: {config_manager.config_dir}") + + # Add project list + if app_config.projects: + config_lines.extend(["", "**Configured Projects**:"]) + for name, path in app_config.projects.items(): + is_default = " (default)" if name == app_config.default_project else "" + config_lines.append(f"- {name}: {path}{is_default}") + + except Exception as e: + logger.debug(f"Could not get configuration info: {e}") + config_lines.append(f"**Configuration Error**: {str(e)}") + + return config_lines + + +def _get_tool_availability() -> list[str]: + """Get available MCP tools for diagnostic output.""" + tool_lines = [] + + try: + # List of expected tools based on the __init__.py file + expected_tools = [ + "build_context", "canvas", "create_project", "delete_note", + "delete_project", "edit_note", "get_current_project", "list_directory", + "list_projects", "move_note", "read_content", "read_note", + "recent_activity", "search_notes", "set_default_project", + "switch_project", "sync_status", "view_note", "write_note", + "diagnostics" # Include this tool itself + ] + + tool_lines.extend([ + f"**Available Tools**: {len(expected_tools)}", + "", + "**Tool List**:" + ]) + + # Group tools by category for better readability + categories = { + "Content Management": [ + "write_note", "read_note", "edit_note", "delete_note", + "move_note", "view_note", "read_content" + ], + "Project Management": [ + "list_projects", "switch_project", "get_current_project", + "create_project", "delete_project", "set_default_project" + ], + "Navigation & Discovery": [ + "build_context", "list_directory", "search_notes", "recent_activity" + ], + "Visualization": ["canvas"], + "System": ["sync_status", "diagnostics"] + } + + for category, tools in categories.items(): + tool_lines.append(f"- **{category}**: {', '.join(tools)}") + + except Exception as e: + logger.debug(f"Could not get tool availability: {e}") + tool_lines.append(f"**Tool Availability Error**: {str(e)}") + + return tool_lines + + +@mcp.tool( + description="""Get comprehensive diagnostic information for Basic Memory installation. + + This tool provides: + - Basic Memory version and build information + - System environment details (Python, OS, architecture) + - Configuration summary and project status + - Available MCP tools and their categories + - Current project context + + Use this tool to: + - Troubleshoot installation issues + - Verify feature availability + - Confirm system compatibility + - Support debugging and issue reporting + - Check configuration status + """, +) +async def diagnostics(project: Optional[str] = None) -> str: + """Get comprehensive diagnostic information for Basic Memory. + + This tool provides detailed information about the Basic Memory installation, + system environment, configuration, and available functionality. + + Args: + project: Optional project name to include project-specific context + + Returns: + Comprehensive diagnostic report with version, system, and status information + """ + logger.info("MCP tool call tool=diagnostics") + + diagnostic_lines = [] + + try: + # Header + diagnostic_lines.extend([ + "# Basic Memory Diagnostics", + "", + f"**Version**: {basic_memory.__version__}", + f"**API Version**: {basic_memory.__api_version__}", + "", + ]) + + # System Information + diagnostic_lines.extend(["## System Information", ""]) + diagnostic_lines.extend(_get_system_info()) + diagnostic_lines.extend(["", ""]) + + # Configuration Summary + diagnostic_lines.extend(["## Configuration", ""]) + diagnostic_lines.extend(_get_configuration_summary()) + diagnostic_lines.extend(["", ""]) + + # Tool Availability + diagnostic_lines.extend(["## Tool Availability", ""]) + diagnostic_lines.extend(_get_tool_availability()) + diagnostic_lines.extend(["", ""]) + + # Project Context + diagnostic_lines.extend(["## Project Context", ""]) + if project: + try: + active_project = get_active_project(project) + diagnostic_lines.extend([ + f"**Requested Project**: {project}", + f"**Active Project Name**: {active_project.name}", + f"**Project Path**: {active_project.home}", + ]) + except Exception as e: + diagnostic_lines.extend([ + f"**Requested Project**: {project}", + f"**Project Error**: {str(e)}", + ]) + else: + try: + from basic_memory.config import app_config + diagnostic_lines.extend([ + f"**Default Project**: {app_config.default_project}", + "**Note**: Use the 'project' parameter to get specific project context", + ]) + except Exception as e: + diagnostic_lines.append(f"**Project Context Error**: {str(e)}") + + diagnostic_lines.extend(["", ""]) + + # Footer with usage info + diagnostic_lines.extend([ + "## Usage Notes", + "", + "- This diagnostic tool helps troubleshoot Basic Memory installations", + "- Share this output when reporting issues for faster support", + "- All sensitive paths and personal information should be reviewed before sharing", + "- For more detailed logging, check the Basic Memory log files in ~/.basic-memory/", + ]) + + return "\n".join(diagnostic_lines) + + except Exception as e: + return f"""# Basic Memory Diagnostics - Error + +❌ **Unable to generate diagnostic information**: {str(e)} + +**Troubleshooting:** +- The system may still be starting up +- Try waiting a few seconds and checking again +- Check logs for detailed error information +- Consider restarting if the issue persists + +**Basic Information:** +- Basic Memory Version: {getattr(basic_memory, '__version__', 'Unknown')} +- Python Version: {sys.version.split()[0]} +- Platform: {platform.system()} {platform.release()} +""" \ 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..edeb5bb7 --- /dev/null +++ b/tests/mcp/test_tool_diagnostics.py @@ -0,0 +1,185 @@ +"""Tests for diagnostics MCP tool.""" + +import pytest +from unittest.mock import MagicMock, patch +import platform +import sys + +from basic_memory.mcp.tools.diagnostics import diagnostics +import basic_memory + + +@pytest.mark.asyncio +async def test_diagnostics_basic_functionality(): + """Test diagnostics tool returns expected basic information.""" + result = await diagnostics.fn() + + # Check basic structure + assert "# Basic Memory Diagnostics" in result + assert f"**Version**: {basic_memory.__version__}" in result + assert f"**API Version**: {basic_memory.__api_version__}" in result + + # Check system information + assert "## System Information" in result + assert f"**Python Version**: {sys.version.split()[0]}" in result + assert f"**Platform**: {platform.system()}" in result + assert f"**Architecture**: {platform.machine()}" in result + + # Check tool availability section + assert "## Tool Availability" in result + assert "**Available Tools**:" in result + assert "Content Management" in result + assert "Project Management" in result + + # Check configuration section + assert "## Configuration" in result + + # Check project context section + assert "## Project Context" in result + + # Check usage notes + assert "## Usage Notes" in result + + +@pytest.mark.asyncio +async def test_diagnostics_with_project(): + """Test diagnostics tool with project parameter.""" + # Mock get_active_project to return a test project + mock_project = MagicMock() + mock_project.name = "test-project" + mock_project.home = "/path/to/test-project" + + with patch("basic_memory.mcp.tools.diagnostics.get_active_project", return_value=mock_project): + result = await diagnostics.fn(project="test-project") + + assert "**Requested Project**: test-project" in result + assert "**Active Project Name**: test-project" in result + assert "**Project Path**: /path/to/test-project" in result + + +@pytest.mark.asyncio +async def test_diagnostics_with_invalid_project(): + """Test diagnostics tool with invalid project parameter.""" + # Mock get_active_project to raise an exception + with patch("basic_memory.mcp.tools.diagnostics.get_active_project", side_effect=ValueError("Project not found")): + result = await diagnostics.fn(project="invalid-project") + + assert "**Requested Project**: invalid-project" in result + assert "**Project Error**: Project not found" in result + + +@pytest.mark.asyncio +async def test_diagnostics_configuration_section(): + """Test that configuration section includes expected information.""" + # Mock app_config for predictable testing + mock_config = MagicMock() + mock_config.env = "test" + mock_config.log_level = "INFO" + mock_config.default_project = "main" + mock_config.projects = {"main": "/home/user/basic-memory", "work": "/home/user/work-notes"} + mock_config.sync_delay = 1000 + mock_config.update_permalinks_on_move = False + mock_config.sync_changes = True + mock_config.app_database_path = "/home/user/.basic-memory/memory.db" + + mock_config_manager = MagicMock() + mock_config_manager.config_dir = "/home/user/.basic-memory" + + with patch("basic_memory.mcp.tools.diagnostics.app_config", mock_config), \ + patch("basic_memory.mcp.tools.diagnostics.config_manager", mock_config_manager): + result = await diagnostics.fn() + + assert "**Environment**: test" in result + assert "**Log Level**: INFO" in result + assert "**Default Project**: main" in result + assert "**Total Projects**: 2" in result + assert "**Sync Delay**: 1000ms" in result + assert "**Update Permalinks on Move**: False" in result + assert "**Sync Changes**: True" in result + assert "**Database Path**: /home/user/.basic-memory/memory.db" in result + assert "**Config Directory**: /home/user/.basic-memory" in result + assert "- main: /home/user/basic-memory (default)" in result + assert "- work: /home/user/work-notes" in result + + +@pytest.mark.asyncio +async def test_diagnostics_handles_configuration_error(): + """Test diagnostics tool handles configuration errors gracefully.""" + # Mock configuration to raise an exception + with patch("basic_memory.mcp.tools.diagnostics.app_config", side_effect=ImportError("Config error")): + result = await diagnostics.fn() + + # Should still return basic information + assert "# Basic Memory Diagnostics" in result + assert f"**Version**: {basic_memory.__version__}" in result + assert "**Configuration Error**: Config error" in result + + +@pytest.mark.asyncio +async def test_diagnostics_exception_handling(): + """Test diagnostics tool handles major exceptions gracefully.""" + # Mock basic_memory to raise an exception when accessing __version__ + with patch.object(basic_memory, '__version__', side_effect=AttributeError("Version error")): + result = await diagnostics.fn() + + # Should return error format + assert "# Basic Memory Diagnostics - Error" in result + assert "Unable to generate diagnostic information" in result + assert "Troubleshooting:" in result + + +def test_get_system_info(): + """Test the _get_system_info helper function.""" + from basic_memory.mcp.tools.diagnostics import _get_system_info + + info_lines = _get_system_info() + + # Should include basic system info + assert any("**Python Version**:" in line for line in info_lines) + assert any("**Platform**:" in line for line in info_lines) + assert any("**Architecture**:" in line for line in info_lines) + assert any("**Python Implementation**:" in line for line in info_lines) + + +def test_get_tool_availability(): + """Test the _get_tool_availability helper function.""" + from basic_memory.mcp.tools.diagnostics import _get_tool_availability + + tool_lines = _get_tool_availability() + + # Should include tool categories + assert any("**Available Tools**:" in line for line in tool_lines) + assert any("**Content Management**:" in line for line in tool_lines) + assert any("**Project Management**:" in line for line in tool_lines) + assert any("**Navigation & Discovery**:" in line for line in tool_lines) + assert any("**Visualization**:" in line for line in tool_lines) + assert any("**System**:" in line for line in tool_lines) + + +def test_get_configuration_summary_with_mocked_config(): + """Test the _get_configuration_summary helper function with mocked config.""" + from basic_memory.mcp.tools.diagnostics import _get_configuration_summary + + # Mock the config imports + mock_config = MagicMock() + mock_config.env = "test" + mock_config.log_level = "DEBUG" + mock_config.default_project = "main" + mock_config.projects = {"main": "/test/path"} + mock_config.sync_delay = 500 + mock_config.update_permalinks_on_move = True + mock_config.sync_changes = False + mock_config.app_database_path = "/test/db.sqlite" + + mock_config_manager = MagicMock() + mock_config_manager.config_dir = "/test/config" + + with patch("basic_memory.mcp.tools.diagnostics.app_config", mock_config), \ + patch("basic_memory.mcp.tools.diagnostics.config_manager", mock_config_manager): + config_lines = _get_configuration_summary() + + # Should include all expected config info + assert any("**Environment**: test" in line for line in config_lines) + assert any("**Log Level**: DEBUG" in line for line in config_lines) + assert any("**Default Project**: main" in line for line in config_lines) + assert any("**Total Projects**: 1" in line for line in config_lines) \ No newline at end of file