mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
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 <groksrc@users.noreply.github.com>
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user