mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a4fd71c10b |
@@ -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",
|
||||
|
||||
@@ -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()}
|
||||
"""
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user