Skip to main content
Glama

MCP Memory Service

#!/usr/bin/env python3 """ Production-ready script to analyze timestamp health in MCP Memory Service databases. This tool provides comprehensive timestamp analysis for SQLite-based memory storage, helping identify and diagnose timestamp-related issues that could affect search functionality. """ import sys import sqlite3 import json import argparse import logging from datetime import datetime from pathlib import Path from typing import Optional, Dict, Any # Configure logging logging.basicConfig( level=logging.INFO, format='%(levelname)s: %(message)s' ) logger = logging.getLogger(__name__) def analyze_timestamps(db_path: str, output_format: str = 'text', verbose: bool = False) -> Dict[str, Any]: """Analyze timestamp fields directly in the database. Args: db_path: Path to the SQLite database file output_format: Output format ('text', 'json', or 'summary') verbose: Enable verbose output Returns: Dictionary containing analysis results """ results = {} if output_format == 'text': print(f"=== Analyzing timestamps in {db_path} ===") # Validate database path db_file = Path(db_path) if not db_file.exists(): error_msg = f"Database file not found: {db_path}" logger.error(error_msg) return {'error': error_msg, 'success': False} try: conn = sqlite3.connect(db_path) conn.row_factory = sqlite3.Row # Get basic stats cursor = conn.execute("SELECT COUNT(*) as total FROM memories") total_count = cursor.fetchone()['total'] results['total_memories'] = total_count if output_format == 'text': print(f"📊 Total memories in database: {total_count}") # Analyze timestamp fields cursor = conn.execute(""" SELECT COUNT(*) as total, COUNT(created_at) as has_created_at, COUNT(created_at_iso) as has_created_at_iso, COUNT(CASE WHEN created_at IS NULL AND created_at_iso IS NULL THEN 1 END) as missing_both, MIN(created_at) as earliest_ts, MAX(created_at) as latest_ts FROM memories """) stats = cursor.fetchone() # Store results results['timestamp_stats'] = { 'total': stats['total'], 'has_created_at': stats['has_created_at'], 'has_created_at_iso': stats['has_created_at_iso'], 'missing_both': stats['missing_both'], 'missing_created_at': stats['total'] - stats['has_created_at'], 'missing_created_at_iso': stats['total'] - stats['has_created_at_iso'] } if output_format == 'text': print(f"\n🕐 TIMESTAMP ANALYSIS:") print(f" Total entries: {stats['total']}") print(f" Has created_at (float): {stats['has_created_at']}") print(f" Has created_at_iso (ISO): {stats['has_created_at_iso']}") print(f" Missing both timestamps: {stats['missing_both']}") if output_format == 'text': if stats['has_created_at'] > 0: missing_created_at = stats['total'] - stats['has_created_at'] print(f" Missing created_at: {missing_created_at}") if stats['has_created_at_iso'] > 0: missing_created_at_iso = stats['total'] - stats['has_created_at_iso'] print(f" Missing created_at_iso: {missing_created_at_iso}") # Show timestamp range if stats['earliest_ts'] and stats['latest_ts']: earliest = datetime.fromtimestamp(stats['earliest_ts']) latest = datetime.fromtimestamp(stats['latest_ts']) results['timestamp_range'] = { 'earliest': earliest.isoformat(), 'latest': latest.isoformat(), 'earliest_float': stats['earliest_ts'], 'latest_float': stats['latest_ts'] } if output_format == 'text': print(f"\n📅 TIMESTAMP RANGE:") print(f" Earliest: {earliest} ({stats['earliest_ts']})") print(f" Latest: {latest} ({stats['latest_ts']})") # Find problematic entries cursor = conn.execute(""" SELECT id, content_hash, created_at, created_at_iso, SUBSTR(content, 1, 100) as content_preview FROM memories WHERE created_at IS NULL AND created_at_iso IS NULL LIMIT 10 """) problematic = cursor.fetchall() results['missing_both_examples'] = len(problematic) if output_format == 'text' and problematic: print(f"\n⚠️ ENTRIES MISSING BOTH TIMESTAMPS ({len(problematic)} shown):") for row in problematic: print(f" ID {row['id']}: {row['content_preview']}...") if verbose: print(f" Hash: {row['content_hash']}") print(f" created_at: {row['created_at']}") print(f" created_at_iso: {row['created_at_iso']}") print() # Find entries with only one timestamp type cursor = conn.execute(""" SELECT COUNT(*) as count FROM memories WHERE (created_at IS NULL AND created_at_iso IS NOT NULL) OR (created_at IS NOT NULL AND created_at_iso IS NULL) """) partial_timestamps = cursor.fetchone()['count'] results['partial_timestamps'] = partial_timestamps if output_format == 'text' and partial_timestamps > 0: print(f"\n⚠️ ENTRIES WITH PARTIAL TIMESTAMPS: {partial_timestamps}") # Show some examples cursor = conn.execute(""" SELECT id, content_hash, created_at, created_at_iso, SUBSTR(content, 1, 60) as content_preview FROM memories WHERE (created_at IS NULL AND created_at_iso IS NOT NULL) OR (created_at IS NOT NULL AND created_at_iso IS NULL) LIMIT 5 """) examples = cursor.fetchall() if output_format == 'text' and verbose: for row in examples: print(f" ID {row['id']}: {row['content_preview']}...") print(f" created_at: {row['created_at']}") print(f" created_at_iso: {row['created_at_iso']}") print() # Health assessment health_status = 'EXCELLENT' health_message = 'All memories have complete timestamps' if stats['missing_both'] > 0: if stats['missing_both'] < stats['total'] * 0.01: health_status = 'GOOD' health_message = f"Only {stats['missing_both']}/{stats['total']} missing all timestamps" elif stats['missing_both'] < stats['total'] * 0.1: health_status = 'WARNING' health_message = f"{stats['missing_both']}/{stats['total']} missing all timestamps" else: health_status = 'CRITICAL' health_message = f"{stats['missing_both']}/{stats['total']} missing all timestamps" results['health'] = { 'status': health_status, 'message': health_message, 'partial_timestamps': partial_timestamps } if output_format == 'text': print(f"\n🏥 DATABASE HEALTH:") emoji = {'EXCELLENT': '✅', 'GOOD': '✅', 'WARNING': '⚠️', 'CRITICAL': '❌'} print(f" {emoji.get(health_status, '?')} {health_status}: {health_message}") if partial_timestamps > 0: print(f" ⚠️ {partial_timestamps} entries have only partial timestamp data") else: print(" ✅ All entries with timestamps have both float and ISO formats") conn.close() results['success'] = True return results except sqlite3.OperationalError as e: if 'no such table: memories' in str(e): error_msg = "Database does not contain 'memories' table. Is this a valid MCP Memory Service database?" else: error_msg = f"Database error: {e}" logger.error(error_msg) results['error'] = error_msg results['success'] = False except Exception as e: error_msg = f"Unexpected error: {e}" logger.error(error_msg) results['error'] = error_msg results['success'] = False finally: if 'conn' in locals(): conn.close() return results def main(): """Main entry point with CLI argument parsing.""" # Set up argument parser parser = argparse.ArgumentParser( description='Analyze timestamp health in MCP Memory Service SQLite databases', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: %(prog)s # Use default database path %(prog)s /path/to/database.db # Analyze specific database %(prog)s -f json -o results.json # Output JSON to file %(prog)s --verbose # Show detailed analysis %(prog)s --format summary # Quick health check only """ ) # Default database path for macOS default_db_path = Path.home() / "Library" / "Application Support" / "mcp-memory" / "sqlite_vec.db" parser.add_argument( 'database', nargs='?', default=str(default_db_path), help=f'Path to SQLite database (default: {default_db_path})' ) parser.add_argument( '-f', '--format', choices=['text', 'json', 'summary'], default='text', help='Output format (default: text)' ) parser.add_argument( '-o', '--output', help='Output file path (default: stdout)' ) parser.add_argument( '-v', '--verbose', action='store_true', help='Show verbose output with additional details' ) parser.add_argument( '--quiet', action='store_true', help='Suppress all output except errors' ) args = parser.parse_args() # Configure logging based on verbosity if args.quiet: logging.getLogger().setLevel(logging.ERROR) elif args.verbose: logging.getLogger().setLevel(logging.DEBUG) # Analyze the database results = analyze_timestamps(args.database, args.format, args.verbose) # Handle output if args.format == 'json': output = json.dumps(results, indent=2, default=str) if args.output: with open(args.output, 'w') as f: f.write(output) if not args.quiet: print(f"Results written to {args.output}") else: print(output) elif args.format == 'summary': if results.get('success'): health = results.get('health', {}) print(f"Status: {health.get('status', 'UNKNOWN')}") print(f"Message: {health.get('message', 'No health data')}") print(f"Total Memories: {results.get('total_memories', 0)}") missing = results.get('timestamp_stats', {}).get('missing_both', 0) if missing > 0: print(f"Missing Timestamps: {missing}") else: print(f"Error: {results.get('error', 'Unknown error')}") # Return appropriate exit code if results.get('success'): health_status = results.get('health', {}).get('status', 'UNKNOWN') if health_status in ['EXCELLENT', 'GOOD']: sys.exit(0) elif health_status == 'WARNING': sys.exit(1) else: sys.exit(2) else: sys.exit(3) if __name__ == "__main__": main()

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