Skip to main content
Glama

MCP Memory Service

#!/usr/bin/env python3 # Copyright 2024 Heinrich Krupp # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Utilities for installing and managing Claude Code commands for MCP Memory Service. """ import os import sys import shutil import subprocess from pathlib import Path from datetime import datetime from typing import Optional, List, Dict, Tuple def print_info(text: str) -> None: """Print formatted info text.""" print(f" -> {text}") def print_error(text: str) -> None: """Print formatted error text.""" print(f" [ERROR] {text}") def print_success(text: str) -> None: """Print formatted success text.""" print(f" [OK] {text}") def print_warning(text: str) -> None: """Print formatted warning text.""" print(f" [WARNING] {text}") def check_claude_code_cli() -> Tuple[bool, Optional[str]]: """ Check if Claude Code CLI is installed and available. Returns: Tuple of (is_available, version_or_error) """ try: # Try to run claude --version result = subprocess.run( ['claude', '--version'], capture_output=True, text=True, timeout=10 ) if result.returncode == 0: version = result.stdout.strip() return True, version else: return False, f"claude command failed: {result.stderr.strip()}" except subprocess.TimeoutExpired: return False, "claude command timed out" except FileNotFoundError: return False, "claude command not found in PATH" except Exception as e: return False, f"Error checking claude CLI: {str(e)}" def get_claude_commands_directory() -> Path: """ Get the Claude Code commands directory path. Returns: Path to ~/.claude/commands/ """ return Path.home() / ".claude" / "commands" def get_claude_hooks_directory() -> Path: """ Get the Claude Code hooks directory path. Returns: Path to ~/.claude/hooks/ """ return Path.home() / ".claude" / "hooks" def check_for_legacy_claude_paths() -> Tuple[bool, List[str]]: """ Check for legacy .claude-code directory structure and provide migration guidance. Returns: Tuple of (legacy_found, list_of_issues_and_recommendations) """ issues = [] legacy_found = False # Check for legacy .claude-code directories legacy_paths = [ Path.home() / ".claude-code", Path.home() / ".claude-code" / "hooks", Path.home() / ".claude-code" / "commands" ] for legacy_path in legacy_paths: if legacy_path.exists(): legacy_found = True issues.append(f"⚠ Found legacy directory: {legacy_path}") # Check what's in the legacy directory if legacy_path.name == "hooks" and any(legacy_path.glob("*.js")): issues.append(f" → Contains hook files that should be moved to ~/.claude/hooks/") elif legacy_path.name == "commands" and any(legacy_path.glob("*.md")): issues.append(f" → Contains command files that should be moved to ~/.claude/commands/") if legacy_found: issues.append("") issues.append("Migration steps:") issues.append("1. Create new directory: ~/.claude/") issues.append("2. Move hooks: ~/.claude-code/hooks/* → ~/.claude/hooks/") issues.append("3. Move commands: ~/.claude-code/commands/* → ~/.claude/commands/") issues.append("4. Update settings.json to reference new paths") issues.append("5. Remove old ~/.claude-code/ directory when satisfied") return legacy_found, issues def validate_claude_settings_paths() -> Tuple[bool, List[str]]: """ Validate paths in Claude settings files and detect common Windows path issues. Returns: Tuple of (all_valid, list_of_issues_and_recommendations) """ import json import platform issues = [] all_valid = True # Common Claude settings locations settings_paths = [ Path.home() / ".claude" / "settings.json", Path.home() / ".claude" / "settings.local.json" ] for settings_path in settings_paths: if not settings_path.exists(): continue try: with open(settings_path, 'r', encoding='utf-8') as f: settings = json.load(f) # Check hooks configuration if 'hooks' in settings: for hook in settings.get('hooks', []): if 'command' in hook: command = hook['command'] # Check for Windows path issues if platform.system() == "Windows": if '\\' in command and not command.startswith('"'): all_valid = False issues.append(f"⚠ Windows path with backslashes in {settings_path.name}:") issues.append(f" → {command}") issues.append(f" → Consider using forward slashes: {command.replace(chr(92), '/')}") # Check for legacy .claude-code references if '.claude-code' in command: all_valid = False issues.append(f"⚠ Legacy path reference in {settings_path.name}:") issues.append(f" → {command}") issues.append(f" → Update to use .claude instead of .claude-code") # Check for missing session-start-wrapper.bat if 'session-start-wrapper.bat' in command: all_valid = False issues.append(f"⚠ Reference to non-existent wrapper file in {settings_path.name}:") issues.append(f" → {command}") issues.append(f" → Use Node.js script directly: node path/to/session-start.js") # Check if referenced files exist if command.startswith('node '): script_path_str = command.replace('node ', '').strip() # Handle quoted paths if script_path_str.startswith('"') and script_path_str.endswith('"'): script_path_str = script_path_str[1:-1] script_path = Path(script_path_str) if not script_path.exists() and not script_path.is_absolute(): # Try to resolve relative to home directory script_path = Path.home() / script_path_str if not script_path.exists(): all_valid = False issues.append(f"⚠ Hook script not found: {script_path_str}") issues.append(f" → Check if hooks are properly installed") except json.JSONDecodeError as e: all_valid = False issues.append(f"⚠ JSON parsing error in {settings_path.name}: {str(e)}") except Exception as e: all_valid = False issues.append(f"⚠ Error reading {settings_path.name}: {str(e)}") return all_valid, issues def normalize_windows_path_for_json(path_str: str) -> str: """ Normalize a Windows path for use in JSON configuration files. Args: path_str: Path string that may contain backslashes Returns: Path string with forward slashes suitable for JSON """ import platform if platform.system() == "Windows": # Convert backslashes to forward slashes normalized = path_str.replace('\\', '/') # Handle double backslashes from escaped strings normalized = normalized.replace('//', '/') return normalized return path_str def check_commands_directory_access() -> Tuple[bool, str]: """ Check if we can access and write to the Claude commands directory. Returns: Tuple of (can_access, status_message) """ commands_dir = get_claude_commands_directory() try: # Check if directory exists if not commands_dir.exists(): # Try to create it commands_dir.mkdir(parents=True, exist_ok=True) return True, f"Created commands directory: {commands_dir}" # Check if we can write to it test_file = commands_dir / ".test_write_access" try: test_file.write_text("test") test_file.unlink() return True, f"Commands directory accessible: {commands_dir}" except PermissionError: return False, f"No write permission to commands directory: {commands_dir}" except Exception as e: return False, f"Cannot access commands directory: {str(e)}" def get_source_commands_directory() -> Path: """ Get the source directory containing the command markdown files. Returns: Path to the claude_commands directory in the project """ # Get the directory containing this script script_dir = Path(__file__).parent # Go up one level to the project root and find claude_commands project_root = script_dir.parent return project_root / "claude_commands" def list_available_commands() -> List[Dict[str, str]]: """ List all available command files in the source directory. Returns: List of command info dictionaries """ source_dir = get_source_commands_directory() commands = [] if not source_dir.exists(): return commands for md_file in source_dir.glob("*.md"): # Extract command name from filename command_name = md_file.stem # Read the first line to get the description try: with open(md_file, 'r', encoding='utf-8') as f: first_line = f.readline().strip() # Remove markdown header formatting description = first_line.lstrip('# ').strip() except Exception: description = "Command description unavailable" commands.append({ 'name': command_name, 'file': md_file.name, 'description': description, 'path': str(md_file) }) return commands def backup_existing_commands() -> Optional[str]: """ Create a backup of existing command files before installation. Returns: Path to backup directory if backup was created, None otherwise """ commands_dir = get_claude_commands_directory() if not commands_dir.exists(): return None # Check if there are any existing .md files existing_commands = list(commands_dir.glob("*.md")) if not existing_commands: return None # Create backup directory with timestamp timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") backup_dir = commands_dir / f"backup_{timestamp}" try: backup_dir.mkdir(exist_ok=True) for cmd_file in existing_commands: shutil.copy2(cmd_file, backup_dir / cmd_file.name) print_info(f"Backed up {len(existing_commands)} existing commands to: {backup_dir}") return str(backup_dir) except Exception as e: print_error(f"Failed to create backup: {str(e)}") return None def install_command_files() -> Tuple[bool, List[str]]: """ Install command markdown files to the Claude commands directory. Returns: Tuple of (success, list_of_installed_files) """ source_dir = get_source_commands_directory() commands_dir = get_claude_commands_directory() installed_files = [] if not source_dir.exists(): print_error(f"Source commands directory not found: {source_dir}") return False, [] try: # Ensure destination directory exists commands_dir.mkdir(parents=True, exist_ok=True) # Copy all .md files for md_file in source_dir.glob("*.md"): dest_file = commands_dir / md_file.name shutil.copy2(md_file, dest_file) installed_files.append(md_file.name) print_info(f"Installed: {md_file.name}") if installed_files: print_success(f"Successfully installed {len(installed_files)} Claude Code commands") return True, installed_files else: print_warning("No command files found to install") return False, [] except Exception as e: print_error(f"Failed to install command files: {str(e)}") return False, [] def verify_mcp_service_connectivity() -> Tuple[bool, str]: """ Verify that the MCP Memory Service is accessible. Returns: Tuple of (is_accessible, status_message) """ try: # Try to import the MCP service modules sys.path.insert(0, str(Path(__file__).parent.parent / "src")) # Test basic connectivity from mcp_memory_service import config # Check if we can detect a running service # This is a basic check - in a real scenario, we'd try to connect return True, "MCP Memory Service modules available" except ImportError as e: return False, f"MCP Memory Service not properly installed: {str(e)}" except Exception as e: return False, f"Error checking MCP service: {str(e)}" def test_command_functionality() -> Tuple[bool, List[str]]: """ Test that installed commands are accessible via Claude Code CLI. Returns: Tuple of (all_tests_passed, list_of_test_results) """ commands_dir = get_claude_commands_directory() test_results = [] all_passed = True # Check if command files exist and are readable for md_file in commands_dir.glob("memory-*.md"): try: with open(md_file, 'r', encoding='utf-8') as f: content = f.read() if len(content) > 0: test_results.append(f"✓ {md_file.name} - readable and non-empty") else: test_results.append(f"✗ {md_file.name} - file is empty") all_passed = False except Exception as e: test_results.append(f"✗ {md_file.name} - error reading: {str(e)}") all_passed = False # Try to run claude commands (if Claude CLI is available) claude_available, _ = check_claude_code_cli() if claude_available: try: # Test that claude can see our commands result = subprocess.run( ['claude', '--help'], capture_output=True, text=True, timeout=10 ) if result.returncode == 0: test_results.append("✓ Claude Code CLI is responsive") else: test_results.append("✗ Claude Code CLI returned error") all_passed = False except Exception as e: test_results.append(f"✗ Error testing Claude CLI: {str(e)}") all_passed = False return all_passed, test_results def uninstall_commands() -> Tuple[bool, List[str]]: """ Uninstall MCP Memory Service commands from Claude Code. Returns: Tuple of (success, list_of_removed_files) """ commands_dir = get_claude_commands_directory() removed_files = [] if not commands_dir.exists(): return True, [] # Nothing to remove try: # Remove all memory-*.md files for md_file in commands_dir.glob("memory-*.md"): md_file.unlink() removed_files.append(md_file.name) print_info(f"Removed: {md_file.name}") if removed_files: print_success(f"Successfully removed {len(removed_files)} commands") else: print_info("No MCP Memory Service commands found to remove") return True, removed_files except Exception as e: print_error(f"Failed to uninstall commands: {str(e)}") return False, [] def install_claude_commands(verbose: bool = True) -> bool: """ Main function to install Claude Code commands for MCP Memory Service. Args: verbose: Whether to print detailed progress information Returns: True if installation was successful, False otherwise """ if verbose: print_info("Installing Claude Code commands for MCP Memory Service...") # Check for legacy paths and provide migration guidance legacy_found, legacy_issues = check_for_legacy_claude_paths() if legacy_found: print_warning("Legacy Claude Code directory structure detected:") for issue in legacy_issues: print_info(issue) print_info("") # Validate existing settings paths settings_valid, settings_issues = validate_claude_settings_paths() if not settings_valid: print_warning("Claude settings path issues detected:") for issue in settings_issues: print_info(issue) print_info("") # Check Claude Code CLI availability claude_available, claude_status = check_claude_code_cli() if not claude_available: print_error(f"Claude Code CLI not available: {claude_status}") print_info("Please install Claude Code CLI first: https://claude.ai/code") return False if verbose: print_success(f"Claude Code CLI detected: {claude_status}") # Check commands directory access can_access, access_status = check_commands_directory_access() if not can_access: print_error(access_status) return False if verbose: print_success(access_status) # Create backup of existing commands backup_path = backup_existing_commands() # Install command files install_success, installed_files = install_command_files() if not install_success: return False # Verify MCP service connectivity (optional - warn but don't fail) mcp_available, mcp_status = verify_mcp_service_connectivity() if mcp_available: if verbose: print_success(mcp_status) else: if verbose: print_warning(f"MCP service check: {mcp_status}") print_info("Commands installed but MCP service may need to be started") # Test command functionality if verbose: print_info("Testing installed commands...") tests_passed, test_results = test_command_functionality() for result in test_results: print_info(result) if tests_passed: print_success("All command tests passed") else: print_warning("Some command tests failed - commands may still work") # Show usage instructions if verbose: print_info("\nClaude Code commands installed successfully!") print_info("Available commands:") for cmd_file in installed_files: cmd_name = cmd_file.replace('.md', '') print_info(f" claude /{cmd_name}") print_info("\nExample usage:") print_info(' claude /memory-store "Important decision about architecture"') print_info(' claude /memory-recall "what did we decide last week?"') print_info(' claude /memory-search --tags "architecture,database"') print_info(' claude /memory-health') return True if __name__ == "__main__": # Allow running this script directly for testing import argparse parser = argparse.ArgumentParser(description="Install Claude Code commands for MCP Memory Service") parser.add_argument('--test', action='store_true', help='Test installation without installing') parser.add_argument('--uninstall', action='store_true', help='Uninstall commands') parser.add_argument('--validate', action='store_true', help='Validate Claude configuration paths') parser.add_argument('--quiet', action='store_true', help='Minimal output') args = parser.parse_args() if args.uninstall: success, removed = uninstall_commands() if success: sys.exit(0) else: sys.exit(1) elif args.validate: # Path validation mode print("Claude Code Configuration Validation") print("=" * 40) # Check for legacy paths legacy_found, legacy_issues = check_for_legacy_claude_paths() if legacy_found: print("❌ Legacy paths detected:") for issue in legacy_issues: print(f" {issue}") else: print("✅ No legacy paths found") print() # Validate settings settings_valid, settings_issues = validate_claude_settings_paths() if settings_valid: print("✅ Claude settings paths are valid") else: print("❌ Settings path issues detected:") for issue in settings_issues: print(f" {issue}") sys.exit(0 if settings_valid and not legacy_found else 1) elif args.test: # Test mode - check prerequisites but don't install claude_ok, claude_msg = check_claude_code_cli() access_ok, access_msg = check_commands_directory_access() mcp_ok, mcp_msg = verify_mcp_service_connectivity() print("Claude Code commands installation test:") print(f" Claude CLI: {'✓' if claude_ok else '✗'} {claude_msg}") print(f" Directory access: {'✓' if access_ok else '✗'} {access_msg}") print(f" MCP service: {'✓' if mcp_ok else '⚠'} {mcp_msg}") if claude_ok and access_ok: print("✓ Ready to install Claude Code commands") sys.exit(0) else: print("✗ Prerequisites not met") sys.exit(1) else: # Normal installation success = install_claude_commands(verbose=not args.quiet) sys.exit(0 if success else 1)

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/doobidoo/mcp-memory-service'

If you have feedback or need assistance with the MCP directory API, please join our Discord server