Compare commits

...

1 Commits

Author SHA1 Message Date
claude[bot] b181783cdf 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 <groksrc@users.noreply.github.com>
2025-07-01 19:10:30 +00:00
3 changed files with 418 additions and 0 deletions
+2
View File
@@ -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",
@@ -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')}
"""
@@ -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