From b181783cdf2331df88edbf061737f3be5727dba2 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Tue, 1 Jul 2025 19:10:30 +0000 Subject: [PATCH] feat: implement basic_memory_diagnostics tool for version and system information Add comprehensive diagnostic tool that provides: - Basic Memory version and API version information - System details (Python, OS, architecture) - Configuration summary with JSON dump - 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. Resolves #187 Co-authored-by: Drew Cain --- src/basic_memory/mcp/tools/__init__.py | 2 + .../mcp/tools/basic_memory_diagnostics.py | 203 +++++++++++++++++ .../mcp/test_tool_basic_memory_diagnostics.py | 213 ++++++++++++++++++ 3 files changed, 418 insertions(+) create mode 100644 src/basic_memory/mcp/tools/basic_memory_diagnostics.py create mode 100644 tests/mcp/test_tool_basic_memory_diagnostics.py diff --git a/src/basic_memory/mcp/tools/__init__.py b/src/basic_memory/mcp/tools/__init__.py index f587efe0..615bc071 100644 --- a/src/basic_memory/mcp/tools/__init__.py +++ b/src/basic_memory/mcp/tools/__init__.py @@ -6,6 +6,7 @@ all tools with the MCP server. """ # Import tools to register them with MCP +from basic_memory.mcp.tools.basic_memory_diagnostics import basic_memory_diagnostics from basic_memory.mcp.tools.delete_note import delete_note from basic_memory.mcp.tools.read_content import read_content from basic_memory.mcp.tools.build_context import build_context @@ -29,6 +30,7 @@ from basic_memory.mcp.tools.project_management import ( ) __all__ = [ + "basic_memory_diagnostics", "build_context", "canvas", "create_memory_project", diff --git a/src/basic_memory/mcp/tools/basic_memory_diagnostics.py b/src/basic_memory/mcp/tools/basic_memory_diagnostics.py new file mode 100644 index 00000000..a4d61e4d --- /dev/null +++ b/src/basic_memory/mcp/tools/basic_memory_diagnostics.py @@ -0,0 +1,203 @@ +"""Diagnostic tool for Basic Memory MCP server.""" + +import json +import platform +import sys +from pathlib import Path +from typing import Optional + +from loguru import logger + +import basic_memory +from basic_memory.mcp.server import mcp +from basic_memory.mcp.project_session import get_active_project + + +@mcp.tool( + description="""Get comprehensive diagnostic information for Basic Memory installation. + + Use this tool to: + - Check Basic Memory version and build information + - Get system details (Python, OS, architecture) + - View current configuration settings + - Get project context information + - Troubleshoot installation issues + + This information is helpful for: + - Verifying which version is running + - Troubleshooting compatibility issues + - Gathering system info for support requests + - Confirming configuration settings + """, +) +async def basic_memory_diagnostics(project: Optional[str] = None) -> str: + """Get diagnostic information about Basic Memory installation and system. + + This tool provides comprehensive diagnostic information including version, + system details, configuration, and project context. + + Args: + project: Optional project name to get project-specific context + + Returns: + Formatted diagnostic information with version, system, and config details + """ + logger.info("MCP tool call tool=basic_memory_diagnostics") + + diagnostic_lines = [] + + try: + # Header + diagnostic_lines.extend([ + "# Basic Memory Diagnostics", + "", + ]) + + # Version Information + diagnostic_lines.extend([ + "## Version Information", + "", + f"**Basic Memory Version**: {basic_memory.__version__}", + f"**API Version**: {basic_memory.__api_version__}", + "", + ]) + + # System Information + diagnostic_lines.extend([ + "## System Information", + "", + f"**Python Version**: {sys.version.split()[0]}", + f"**Platform**: {platform.system()} {platform.release()}", + f"**Architecture**: {platform.machine()}", + f"**Python Executable**: {sys.executable}", + "", + ]) + + # Configuration Information + diagnostic_lines.extend([ + "## Configuration", + "", + ]) + + try: + from basic_memory.config import app_config + + # Configuration file path + config_file_path = getattr(app_config, '_config_file_path', 'Unknown') + diagnostic_lines.extend([ + f"**Config File Path**: {config_file_path}", + "", + ]) + + # Dump configuration as JSON + config_dict = { + "env": app_config.env, + "projects": app_config.projects, + "default_project": app_config.default_project, + "log_level": app_config.log_level, + "debug": app_config.debug, + "database_url": app_config.database_url, + } + + diagnostic_lines.extend([ + "**Configuration JSON**:", + "```json", + json.dumps(config_dict, indent=2, default=str), + "```", + "", + ]) + + except Exception as e: + diagnostic_lines.extend([ + f"❌ **Unable to access configuration**: {str(e)}", + "", + "**Troubleshooting:**", + "- The configuration system may not be initialized", + "- Check if Basic Memory is properly installed", + "- Verify configuration file permissions", + "", + ]) + + # Project Context + if project: + diagnostic_lines.extend([ + "## Project Context", + "", + ]) + + 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}", + f"**Project Exists**: {'✅ Yes' if Path(active_project.home).exists() else '❌ No'}", + "", + ]) + except Exception as e: + diagnostic_lines.extend([ + f"**Requested Project**: {project}", + f"❌ **Project Error**: {str(e)}", + "", + "**Troubleshooting:**", + "- Check if the project name is correct", + "- Verify the project is configured in config.json", + "- Ensure project directory exists and is accessible", + "", + ]) + else: + # Show default project info + diagnostic_lines.extend([ + "## Default Project Context", + "", + ]) + + try: + from basic_memory.config import app_config + default_project_name = app_config.default_project + diagnostic_lines.extend([ + f"**Default Project**: {default_project_name}", + ]) + + if default_project_name and default_project_name in app_config.projects: + project_path = app_config.projects[default_project_name] + diagnostic_lines.extend([ + f"**Default Project Path**: {project_path}", + f"**Project Exists**: {'✅ Yes' if Path(project_path).exists() else '❌ No'}", + ]) + + diagnostic_lines.append("") + + except Exception as e: + diagnostic_lines.extend([ + f"❌ **Unable to get default project info**: {str(e)}", + "", + ]) + + # Footer with usage information + diagnostic_lines.extend([ + "---", + "", + "**Usage**: This diagnostic information can help troubleshoot issues and verify your Basic Memory installation.", + "**Support**: Include this information when reporting issues or requesting support.", + ]) + + 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 running diagnostics again +- Check logs for detailed error information +- Verify Basic Memory is properly installed +- Consider restarting if the issue persists + +**Minimal Information Available:** +- Python Version: {sys.version.split()[0]} +- Platform: {platform.system()} {platform.release()} +- Basic Memory Version: {getattr(basic_memory, '__version__', 'Unknown')} +""" \ No newline at end of file diff --git a/tests/mcp/test_tool_basic_memory_diagnostics.py b/tests/mcp/test_tool_basic_memory_diagnostics.py new file mode 100644 index 00000000..a5065f1b --- /dev/null +++ b/tests/mcp/test_tool_basic_memory_diagnostics.py @@ -0,0 +1,213 @@ +"""Tests for basic_memory_diagnostics MCP tool.""" + +import json +import platform +import sys +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +import basic_memory +from basic_memory.mcp.tools.basic_memory_diagnostics import basic_memory_diagnostics + + +@pytest.mark.asyncio +async def test_basic_memory_diagnostics_basic_functionality(): + """Test basic diagnostic functionality without project parameter.""" + result = await basic_memory_diagnostics.fn() + + # Check that key sections are present + assert "# Basic Memory Diagnostics" in result + assert "## Version Information" in result + assert "## System Information" in result + assert "## Configuration" in result + assert "## Default Project Context" in result + + # Check version information + assert f"**Basic Memory Version**: {basic_memory.__version__}" in result + assert f"**API Version**: {basic_memory.__api_version__}" in result + + # Check system information + assert f"**Python Version**: {sys.version.split()[0]}" in result + assert f"**Platform**: {platform.system()} {platform.release()}" in result + assert f"**Architecture**: {platform.machine()}" in result + + +@pytest.mark.asyncio +async def test_basic_memory_diagnostics_with_project_parameter(): + """Test diagnostic with valid project parameter.""" + # Mock project session to return a valid project + mock_project = MagicMock() + mock_project.name = "test-project" + mock_project.home = "/path/to/test-project" + + with patch("basic_memory.mcp.tools.basic_memory_diagnostics.get_active_project") as mock_get_project: + mock_get_project.return_value = mock_project + + with patch("pathlib.Path.exists") as mock_exists: + mock_exists.return_value = True + + result = await basic_memory_diagnostics.fn(project="test-project") + + # Check project-specific information + assert "## Project Context" in result + assert "**Requested Project**: test-project" in result + assert "**Active Project Name**: test-project" in result + assert "**Project Path**: /path/to/test-project" in result + assert "**Project Exists**: ✅ Yes" in result + + +@pytest.mark.asyncio +async def test_basic_memory_diagnostics_with_invalid_project(): + """Test diagnostic with invalid project parameter.""" + with patch("basic_memory.mcp.tools.basic_memory_diagnostics.get_active_project") as mock_get_project: + mock_get_project.side_effect = Exception("Project not found") + + result = await basic_memory_diagnostics.fn(project="invalid-project") + + # Check error handling for invalid project + assert "## Project Context" in result + assert "**Requested Project**: invalid-project" in result + assert "❌ **Project Error**: Project not found" in result + assert "**Troubleshooting:**" in result + + +@pytest.mark.asyncio +async def test_basic_memory_diagnostics_configuration_success(): + """Test diagnostic with successful configuration access.""" + # Mock app config + mock_config = MagicMock() + mock_config.env = "test" + mock_config.projects = {"main": "/path/to/main", "secondary": "/path/to/secondary"} + mock_config.default_project = "main" + mock_config.log_level = "INFO" + mock_config.debug = False + mock_config.database_url = "sqlite:///test.db" + mock_config._config_file_path = "/path/to/config.json" + + with patch("basic_memory.mcp.tools.basic_memory_diagnostics.app_config", mock_config): + result = await basic_memory_diagnostics.fn() + + # Check configuration information + assert "**Config File Path**: /path/to/config.json" in result + assert "**Configuration JSON**:" in result + assert '"env": "test"' in result + assert '"main": "/path/to/main"' in result + assert '"default_project": "main"' in result + + +@pytest.mark.asyncio +async def test_basic_memory_diagnostics_configuration_error(): + """Test diagnostic when configuration access fails.""" + with patch("basic_memory.mcp.tools.basic_memory_diagnostics.app_config") as mock_config: + # Simulate import error for configuration + mock_config.side_effect = ImportError("Configuration not available") + + result = await basic_memory_diagnostics.fn() + + # Check error handling for configuration + assert "❌ **Unable to access configuration**:" in result + assert "**Troubleshooting:**" in result + assert "configuration system may not be initialized" in result + + +@pytest.mark.asyncio +async def test_basic_memory_diagnostics_exception_handling(): + """Test diagnostic handles unexpected exceptions gracefully.""" + # Mock basic_memory module to raise an exception when accessing version + with patch.object(basic_memory, '__version__', side_effect=Exception("Unexpected error")): + result = await basic_memory_diagnostics.fn() + + # Should return error format but still include minimal info + assert "# Basic Memory Diagnostics - Error" in result + assert "❌ **Unable to generate diagnostic information**:" in result + assert "**Troubleshooting:**" in result + assert "**Minimal Information Available:**" in result + + +@pytest.mark.asyncio +async def test_basic_memory_diagnostics_default_project_info(): + """Test diagnostic shows default project information correctly.""" + # Mock app config with default project + mock_config = MagicMock() + mock_config.default_project = "main" + mock_config.projects = {"main": "/path/to/main"} + + with patch("basic_memory.mcp.tools.basic_memory_diagnostics.app_config", mock_config): + with patch("pathlib.Path.exists") as mock_exists: + mock_exists.return_value = True + + result = await basic_memory_diagnostics.fn() + + # Check default project information + assert "## Default Project Context" in result + assert "**Default Project**: main" in result + assert "**Default Project Path**: /path/to/main" in result + assert "**Project Exists**: ✅ Yes" in result + + +@pytest.mark.asyncio +async def test_basic_memory_diagnostics_default_project_missing(): + """Test diagnostic handles missing default project gracefully.""" + # Mock app config with default project that doesn't exist + mock_config = MagicMock() + mock_config.default_project = "main" + mock_config.projects = {"main": "/path/to/nonexistent"} + + with patch("basic_memory.mcp.tools.basic_memory_diagnostics.app_config", mock_config): + with patch("pathlib.Path.exists") as mock_exists: + mock_exists.return_value = False + + result = await basic_memory_diagnostics.fn() + + # Check missing project indication + assert "**Project Exists**: ❌ No" in result + + +@pytest.mark.asyncio +async def test_basic_memory_diagnostics_json_serialization(): + """Test that configuration JSON serialization works correctly.""" + # Mock app config with complex data types + mock_config = MagicMock() + mock_config.env = "test" + mock_config.projects = {"main": "/path/to/main"} + mock_config.default_project = "main" + mock_config.log_level = "DEBUG" + mock_config.debug = True + mock_config.database_url = "sqlite:///memory.db" + + with patch("basic_memory.mcp.tools.basic_memory_diagnostics.app_config", mock_config): + result = await basic_memory_diagnostics.fn() + + # Extract and validate JSON content + json_start = result.find("```json\n") + 8 + json_end = result.find("\n```", json_start) + json_content = result[json_start:json_end] + + # Should be valid JSON + parsed_config = json.loads(json_content) + assert parsed_config["env"] == "test" + assert parsed_config["projects"]["main"] == "/path/to/main" + assert parsed_config["default_project"] == "main" + assert parsed_config["debug"] is True + + +@pytest.mark.asyncio +async def test_basic_memory_diagnostics_project_nonexistent_path(): + """Test diagnostic with project that has non-existent path.""" + # Mock project session to return a project with non-existent path + mock_project = MagicMock() + mock_project.name = "test-project" + mock_project.home = "/nonexistent/path" + + with patch("basic_memory.mcp.tools.basic_memory_diagnostics.get_active_project") as mock_get_project: + mock_get_project.return_value = mock_project + + with patch("pathlib.Path.exists") as mock_exists: + mock_exists.return_value = False + + result = await basic_memory_diagnostics.fn(project="test-project") + + # Check that non-existent path is indicated + assert "**Project Exists**: ❌ No" in result \ No newline at end of file