Skip to main content
Glama

Search History MCP Server

mcp_server.py34.7 kB
import sqlite3 import os import platform import shutil import tempfile import base64 import subprocess from pathlib import Path from datetime import datetime from fastmcp import FastMCP from typing import Literal from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 import plistlib mcp = FastMCP("search-history-mcp") def get_duckduckgo_encryption_key(): """Retrieve the DuckDuckGo encryption key from macOS Keychain.""" try: result = subprocess.run( [ 'security', 'find-generic-password', '-s', 'DuckDuckGo Privacy Browser Encryption Key v2', '-a', 'com.duckduckgo.mobile.ios', '-w' ], capture_output=True, text=True, check=True ) key_b64 = result.stdout.strip() return base64.b64decode(key_b64) except subprocess.CalledProcessError as e: raise Exception(f"Failed to retrieve DuckDuckGo encryption key from keychain: {e}") def decrypt_chacha_poly(encrypted_data: bytes, key: bytes) -> bytes: """Decrypt data encrypted with ChaCha20-Poly1305.""" if not encrypted_data or len(encrypted_data) < 28: raise ValueError("Encrypted data too short") try: chacha = ChaCha20Poly1305(key) nonce = encrypted_data[:12] ciphertext_with_tag = encrypted_data[12:] plaintext = chacha.decrypt(nonce, ciphertext_with_tag, None) return plaintext except Exception as e: raise Exception(f"Decryption failed: {e}") def decode_nskeyedarchiver(data: bytes) -> str: """Decode NSKeyedArchiver data to get the original string or URL.""" try: plist = plistlib.loads(data) if isinstance(plist, dict) and '$objects' in plist: objects = plist['$objects'] for obj in objects: if isinstance(obj, str) and obj and obj != '$null': if obj.startswith('http') or obj.startswith('file'): return obj if len(obj) > 0 and not obj.startswith('NS'): return obj text = data.decode('utf-8', errors='ignore') parts = [p for p in text.split('\x00') if p and len(p) > 3 and not p.startswith('NS')] if parts: for part in sorted(parts, key=len, reverse=True): if part.startswith('http') or part.startswith('file'): return part if len(part) > 5 and part.isprintable(): return part return "[Could not decode]" except Exception as e: return f"[Decode error: {e}]" def decrypt_duckduckgo_field(encrypted_data: bytes, key: bytes) -> str: """Decrypt and decode an encrypted DuckDuckGo field (URL or title).""" if not encrypted_data: return "[Empty]" try: decrypted_data = decrypt_chacha_poly(encrypted_data, key) decoded_string = decode_nskeyedarchiver(decrypted_data) return decoded_string except Exception as e: return f"[Error: {e}]" def cocoa_timestamp_to_datetime(cocoa_timestamp: float) -> str: """Convert Cocoa/Apple timestamp to readable datetime.""" if not cocoa_timestamp or cocoa_timestamp == 0: return "Never" try: mac_epoch = datetime(2001, 1, 1) dt = datetime.fromtimestamp(mac_epoch.timestamp() + cocoa_timestamp) return dt.strftime("%Y-%m-%d %H:%M:%S") except: return "Invalid timestamp" def find_windows_store_app_path(app_name: str) -> Path: """ Find Windows Store app history path by searching for package directory. Args: app_name: Partial name to search for in package folders (e.g., 'Arc', 'DuckDuckGo') Returns: Path to History file if found, None otherwise """ if os.name != 'nt': return None packages_dir = Path.home() / "AppData" / "Local" / "Packages" if not packages_dir.exists(): return None try: # Search for matching package directory for package in packages_dir.iterdir(): if app_name.lower() in package.name.lower(): # Collect all History files, then prioritize history_files = [] for history_file in package.rglob("History"): if history_file.is_file(): path_str = str(history_file) if "User Data" in path_str or "EBWebView" in path_str: history_files.append(history_file) if not history_files: continue # For DuckDuckGo: prefer EBWebView over internalEnvironment\EBWebView # For Arc: prefer User Data path if app_name.lower() == "duckduckgo": # Prioritize the non-internal path for hf in history_files: if "internalEnvironment" not in str(hf): return hf # Fallback to any EBWebView path return history_files[0] else: # For Arc and others, return first match return history_files[0] except Exception: pass return None def get_history_db_path(browser: Literal["brave", "safari", "chrome", "firefox", "edge", "arc", "opera", "duckduckgo"] = "brave") -> Path: """ Get the path to the browser history database. Handles cross-platform paths (Windows, macOS, Linux) for various browsers. Safari only supported on macOS. DuckDuckGo and Arc available on macOS and Windows. Args: browser: Which browser to get history from ("brave", "safari", "chrome", "firefox", "edge", "arc", "opera", or "duckduckgo") """ if browser == "duckduckgo": # DuckDuckGo browser paths if os.name == 'nt': # Windows # Try Windows Store version first store_path = find_windows_store_app_path("DuckDuckGo") if store_path: history_path = store_path else: # Fall back to standard installation (Chromium-based, no encryption) history_path = Path.home() / "AppData" / "Local" / "DuckDuckGo" / "User Data" / "Default" / "History" elif platform.system() == 'Darwin': # macOS # DuckDuckGo for macOS (uses encryption) history_path = Path.home() / "Library/Containers/com.duckduckgo.mobile.ios/Data/Library/Application Support/Database.sqlite" else: # Linux raise ValueError("DuckDuckGo is not available on Linux") return history_path if browser == "safari": # Safari only available on macOS if platform.system() != 'Darwin': raise ValueError("Safari is only available on macOS") return Path.home() / "Library" / "Safari" / "History.db" # Chromium-based browsers (Brave, Chrome, Edge) if browser == "chrome": if os.name == 'nt': # Windows history_path = Path.home() / "AppData" / "Local" / "Google" / "Chrome" / "User Data" / "Default" / "History" elif platform.system() == 'Darwin': # macOS history_path = Path.home() / "Library" / "Application Support" / "Google" / "Chrome" / "Default" / "History" else: # Linux history_path = Path.home() / ".config" / "google-chrome" / "Default" / "History" elif browser == "edge": # Microsoft Edge (Chromium-based) if os.name == 'nt': # Windows history_path = Path.home() / "AppData" / "Local" / "Microsoft" / "Edge" / "User Data" / "Default" / "History" elif platform.system() == 'Darwin': # macOS history_path = Path.home() / "Library" / "Application Support" / "Microsoft Edge" / "Default" / "History" else: # Linux history_path = Path.home() / ".config" / "microsoft-edge" / "Default" / "History" elif browser == "arc": # Arc Browser (Chromium-based) if os.name == 'nt': # Windows # Try Windows Store version first store_path = find_windows_store_app_path("Arc") if store_path: history_path = store_path else: # Fall back to standard installation history_path = Path.home() / "AppData" / "Local" / "Arc" / "User Data" / "Default" / "History" elif platform.system() == 'Darwin': # macOS history_path = Path.home() / "Library" / "Application Support" / "Arc" / "User Data" / "Default" / "History" else: # Linux raise ValueError("Arc browser is not available on Linux") elif browser == "opera": # Opera (Chromium-based) if os.name == 'nt': # Windows history_path = Path.home() / "AppData" / "Roaming" / "Opera Software" / "Opera Stable" / "Default" / "History" elif platform.system() == 'Darwin': # macOS history_path = Path.home() / "Library" / "Application Support" / "com.operasoftware.Opera" / "Default" / "History" else: # Linux history_path = Path.home() / ".config" / "opera" / "Default" / "History" elif browser == "firefox": # Firefox uses a different structure with profile folders if os.name == 'nt': # Windows profiles_path = Path.home() / "AppData" / "Roaming" / "Mozilla" / "Firefox" / "Profiles" elif platform.system() == 'Darwin': # macOS profiles_path = Path.home() / "Library" / "Application Support" / "Firefox" / "Profiles" else: # Linux profiles_path = Path.home() / ".mozilla" / "firefox" # Firefox stores history in places.sqlite in the profile directory # Look for the default profile (prioritize .default-release over .default) if profiles_path.exists(): # Try .default-release first (newer Firefox versions) profile_folders = list(profiles_path.glob("*.default-release")) if not profile_folders: # Fall back to .default profile_folders = list(profiles_path.glob("*.default")) if profile_folders: # Check if places.sqlite actually exists in the profile for profile in profile_folders: potential_path = profile / "places.sqlite" if potential_path.exists(): history_path = potential_path break else: raise FileNotFoundError(f"places.sqlite not found in Firefox profile folders at {profiles_path}") else: raise FileNotFoundError(f"Firefox profile folder not found in {profiles_path}") else: raise FileNotFoundError(f"Firefox profiles directory not found at {profiles_path}") else: # brave if os.name == 'nt': # Windows history_path = Path.home() / "AppData" / "Local" / "BraveSoftware" / "Brave-Browser" / "User Data" / "Default" / "History" elif platform.system() == 'Darwin': # macOS history_path = Path.home() / "Library" / "Application Support" / "BraveSoftware" / "Brave-Browser" / "Default" / "History" else: # Linux history_path = Path.home() / ".config" / "BraveSoftware" / "Brave-Browser" / "Default" / "History" return history_path def query_history_db(query: str, params: tuple = (), browser: Literal["brave", "safari", "chrome", "firefox", "edge", "arc", "opera"] = "brave") -> list: """ Query the browser history database safely by creating a temporary copy. The database may be locked if browser is running, so we copy it first. Args: query: SQL query to execute params: Query parameters browser: Which browser to query ("brave", "safari", "chrome", "firefox", "edge", "arc", or "opera") """ history_path = get_history_db_path(browser) if not history_path.exists(): raise FileNotFoundError(f"{browser.capitalize()} history database not found at {history_path}") # Create a temporary copy of the database to avoid locking issues with tempfile.NamedTemporaryFile(delete=False, suffix='.db') as tmp_file: tmp_path = tmp_file.name try: shutil.copy2(history_path, tmp_path) except PermissionError as e: # Clean up temp file if copy failed if os.path.exists(tmp_path): os.unlink(tmp_path) if browser == "safari": raise PermissionError( f"Permission denied to access Safari history.\n\n" f"To fix this, grant Full Disk Access:\n" f"1. Open System Settings > Privacy & Security > Full Disk Access\n" f"2. Click the '+' button\n" f"3. Add your terminal app (Terminal.app or iTerm.app) or IDE (VS Code, etc.)\n" f"4. Restart the application\n\n" f"Alternatively, you can access Brave history which doesn't require special permissions." ) from e else: raise try: # Query the temporary database conn = sqlite3.connect(tmp_path) conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute(query, params) results = cursor.fetchall() conn.close() return [dict(row) for row in results] finally: # Clean up temporary file if os.path.exists(tmp_path): os.unlink(tmp_path) def is_duckduckgo_encrypted() -> bool: """Check if DuckDuckGo uses encrypted storage (macOS only).""" return platform.system() == 'Darwin' def query_duckduckgo_db(query: str, params: tuple = ()) -> list: """ Query the DuckDuckGo history database with decryption support. DuckDuckGo stores URLs and titles in encrypted format using ChaCha20-Poly1305. Args: query: SQL query to execute params: Query parameters Returns: List of dictionaries with decrypted data """ # Get encryption key try: key = get_duckduckgo_encryption_key() except Exception as e: raise Exception(f"Failed to get DuckDuckGo encryption key: {e}") history_path = get_history_db_path("duckduckgo") if not history_path.exists(): raise FileNotFoundError(f"DuckDuckGo history database not found at {history_path}") # Create a temporary copy of the database to avoid locking issues with tempfile.NamedTemporaryFile(delete=False, suffix='.db') as tmp_file: tmp_path = tmp_file.name try: shutil.copy2(history_path, tmp_path) except PermissionError as e: # Clean up temp file if copy failed if os.path.exists(tmp_path): os.unlink(tmp_path) raise PermissionError( f"Permission denied to access DuckDuckGo history.\n\n" f"To fix this, grant Full Disk Access:\n" f"1. Open System Settings > Privacy & Security > Full Disk Access\n" f"2. Click the '+' button\n" f"3. Add your terminal app (Terminal.app or iTerm.app) or IDE (VS Code, etc.)\n" f"4. Restart the application" ) from e try: # Query the temporary database conn = sqlite3.connect(tmp_path) cursor = conn.cursor() cursor.execute(query, params) raw_results = cursor.fetchall() conn.close() # Decrypt the results decrypted_results = [] for row in raw_results: # DuckDuckGo schema: ZURLENCRYPTED, ZTITLEENCRYPTED, ZNUMBEROFTOTALVISITS, ZNUMBEROFTRACKERSBLOCKED, ZLASTVISIT url_encrypted, title_encrypted, visits, trackers_blocked, last_visit = row # Decrypt URL and title url = decrypt_duckduckgo_field(url_encrypted, key) title = decrypt_duckduckgo_field(title_encrypted, key) if title_encrypted else "No title" decrypted_results.append({ 'url': url, 'title': title, 'visit_count': visits or 0, 'trackers_blocked': trackers_blocked or 0, 'last_visit_time': last_visit }) return decrypted_results finally: # Clean up temporary file if os.path.exists(tmp_path): os.unlink(tmp_path) def chrome_timestamp_to_datetime(chrome_timestamp: int) -> str: """ Convert Chrome/Brave timestamp (microseconds since 1601-01-01) to readable datetime. """ if not chrome_timestamp or chrome_timestamp == 0: return "Never" try: # Chrome timestamps are in microseconds since January 1, 1601 # Windows epoch: January 1, 1601 # Convert to Unix epoch (January 1, 1970) # The offset is 11644473600 seconds between 1601 and 1970 unix_timestamp = (chrome_timestamp / 1_000_000) - 11644473600 # Validate timestamp is in reasonable range if unix_timestamp < 0 or unix_timestamp > 2147483647: # Max 32-bit timestamp return "Invalid timestamp" dt = datetime.fromtimestamp(unix_timestamp) return dt.strftime("%Y-%m-%d %H:%M:%S") except (ValueError, OSError, OverflowError) as e: return f"Invalid timestamp ({chrome_timestamp})" def safari_timestamp_to_datetime(safari_timestamp: float) -> str: """ Convert Safari timestamp (seconds since 2001-01-01) to readable datetime. """ if not safari_timestamp or safari_timestamp == 0: return "Never" try: # Safari timestamps are seconds since January 1, 2001 (macOS epoch) mac_epoch = datetime(2001, 1, 1) dt = datetime.fromtimestamp(mac_epoch.timestamp() + safari_timestamp) return dt.strftime("%Y-%m-%d %H:%M:%S") except (ValueError, OSError, OverflowError): return f"Invalid timestamp ({safari_timestamp})" def firefox_timestamp_to_datetime(firefox_timestamp: int) -> str: """ Convert Firefox timestamp (microseconds since 1970-01-01) to readable datetime. """ if not firefox_timestamp or firefox_timestamp == 0: return "Never" try: # Firefox timestamps are in microseconds since January 1, 1970 (Unix epoch) dt = datetime.fromtimestamp(firefox_timestamp / 1_000_000) return dt.strftime("%Y-%m-%d %H:%M:%S") except (ValueError, OSError, OverflowError): return f"Invalid timestamp ({firefox_timestamp})" @mcp.tool def search_history(search_term: str, limit: int = 50, browser: Literal["brave", "safari", "chrome", "firefox", "edge", "arc", "opera", "duckduckgo"] = "brave") -> str: """ Search browser history for URLs and titles containing the search term. Args: search_term: The text to search for in URLs and page titles limit: Maximum number of results to return (default: 50, max: 500) browser: Which browser to search ("brave", "safari", "chrome", "firefox", "edge", "arc", "opera", or "duckduckgo") Returns: Formatted list of matching history entries with titles, URLs, and visit times """ if limit > 500: limit = 500 if browser == "duckduckgo": if is_duckduckgo_encrypted(): # DuckDuckGo macOS - encrypted database schema query = """ SELECT ZURLENCRYPTED, ZTITLEENCRYPTED, ZNUMBEROFTOTALVISITS, ZNUMBEROFTRACKERSBLOCKED, ZLASTVISIT FROM ZHISTORYENTRYMANAGEDOBJECT WHERE ZURLENCRYPTED IS NOT NULL ORDER BY ZLASTVISIT DESC LIMIT ? """ # DuckDuckGo needs special handling - decrypt first, then filter all_results = query_duckduckgo_db(query, (limit * 2,)) # Get more results to account for filtering # Filter results by search term after decryption results = [ entry for entry in all_results if search_term.lower() in entry['url'].lower() or search_term.lower() in entry['title'].lower() ][:limit] else: # DuckDuckGo Windows - Chromium-based, no encryption query = """ SELECT url, title, visit_count, last_visit_time FROM urls WHERE (url LIKE ? OR title LIKE ?) AND url NOT LIKE 'https://static.ddg.local/%' ORDER BY last_visit_time DESC LIMIT ? """ search_pattern = f"%{search_term}%" results = query_history_db(query, (search_pattern, search_pattern, limit), browser) elif browser == "safari": # Safari database schema query = """ SELECT history_items.url as url, history_visits.title as title, COUNT(history_visits.id) as visit_count, MAX(history_visits.visit_time) as last_visit_time FROM history_items JOIN history_visits ON history_items.id = history_visits.history_item WHERE history_items.url LIKE ? OR history_visits.title LIKE ? GROUP BY history_items.url ORDER BY last_visit_time DESC LIMIT ? """ elif browser == "firefox": # Firefox database schema (places.sqlite) query = """ SELECT moz_places.url as url, moz_places.title as title, moz_places.visit_count as visit_count, moz_places.last_visit_date as last_visit_time FROM moz_places WHERE (moz_places.url LIKE ? OR moz_places.title LIKE ?) AND moz_places.hidden = 0 ORDER BY last_visit_time DESC LIMIT ? """ else: # Chromium-based browsers (Brave/Chrome/Edge/Arc/Opera) database schema query = """ SELECT url, title, visit_count, last_visit_time FROM urls WHERE url LIKE ? OR title LIKE ? ORDER BY last_visit_time DESC LIMIT ? """ # Query databases (DuckDuckGo already queried above) if browser != "duckduckgo": search_pattern = f"%{search_term}%" results = query_history_db(query, (search_pattern, search_pattern, limit), browser) if not results: return f"No history entries found matching '{search_term}' in {browser.capitalize()}" output = [f"Found {len(results)} {browser.capitalize()} history entries matching '{search_term}':\n"] for i, entry in enumerate(results, 1): title = entry['title'] or "No title" url = entry['url'] visit_count = entry['visit_count'] if browser == "duckduckgo": if is_duckduckgo_encrypted(): # macOS encrypted version has trackers blocked last_visit = cocoa_timestamp_to_datetime(entry['last_visit_time']) trackers_blocked = entry.get('trackers_blocked', 0) output.append(f"{i}. {title}") output.append(f" URL: {url}") output.append(f" Visits: {visit_count} | Trackers blocked: {trackers_blocked} | Last visited: {last_visit}") output.append("") else: # Windows Chromium version last_visit = chrome_timestamp_to_datetime(entry['last_visit_time']) output.append(f"{i}. {title}") output.append(f" URL: {url}") output.append(f" Visits: {visit_count} | Last visited: {last_visit}") output.append("") elif browser == "safari": last_visit = safari_timestamp_to_datetime(entry['last_visit_time']) output.append(f"{i}. {title}") output.append(f" URL: {url}") output.append(f" Visits: {visit_count} | Last visited: {last_visit}") output.append("") elif browser == "firefox": last_visit = firefox_timestamp_to_datetime(entry['last_visit_time']) output.append(f"{i}. {title}") output.append(f" URL: {url}") output.append(f" Visits: {visit_count} | Last visited: {last_visit}") output.append("") else: last_visit = chrome_timestamp_to_datetime(entry['last_visit_time']) output.append(f"{i}. {title}") output.append(f" URL: {url}") output.append(f" Visits: {visit_count} | Last visited: {last_visit}") output.append("") return "\n".join(output) @mcp.tool def get_recent_history(limit: int = 50, browser: Literal["brave", "safari", "chrome", "firefox", "edge", "arc", "opera", "duckduckgo"] = "brave") -> str: """ Get the most recent browsing history entries. Args: limit: Maximum number of results to return (default: 50, max: 500) browser: Which browser to query ("brave", "safari", "chrome", "firefox", "edge", "arc", "opera", or "duckduckgo") Returns: Formatted list of recent history entries with titles, URLs, and visit times """ if limit > 500: limit = 500 if browser == "duckduckgo": if is_duckduckgo_encrypted(): # DuckDuckGo macOS - encrypted database schema query = """ SELECT ZURLENCRYPTED, ZTITLEENCRYPTED, ZNUMBEROFTOTALVISITS, ZNUMBEROFTRACKERSBLOCKED, ZLASTVISIT FROM ZHISTORYENTRYMANAGEDOBJECT WHERE ZURLENCRYPTED IS NOT NULL ORDER BY ZLASTVISIT DESC LIMIT ? """ results = query_duckduckgo_db(query, (limit,)) else: # DuckDuckGo Windows - Chromium-based query = """ SELECT url, title, visit_count, last_visit_time FROM urls WHERE url NOT LIKE 'https://static.ddg.local/%' ORDER BY last_visit_time DESC LIMIT ? """ results = query_history_db(query, (limit,), browser) elif browser == "safari": # Safari database schema query = """ SELECT history_items.url as url, history_visits.title as title, COUNT(history_visits.id) as visit_count, MAX(history_visits.visit_time) as last_visit_time FROM history_items JOIN history_visits ON history_items.id = history_visits.history_item GROUP BY history_items.url ORDER BY last_visit_time DESC LIMIT ? """ elif browser == "firefox": # Firefox database schema (places.sqlite) query = """ SELECT url, title, visit_count, last_visit_date as last_visit_time FROM moz_places WHERE hidden = 0 ORDER BY last_visit_time DESC LIMIT ? """ else: # Chromium-based browsers (Brave/Chrome/Edge/Arc/Opera) database schema query = """ SELECT url, title, visit_count, last_visit_time FROM urls ORDER BY last_visit_time DESC LIMIT ? """ # Query databases (DuckDuckGo already queried above) if browser != "duckduckgo": results = query_history_db(query, (limit,), browser) if not results: return f"No history entries found in {browser.capitalize()}" output = [f"Most recent {len(results)} {browser.capitalize()} browsing history entries:\n"] for i, entry in enumerate(results, 1): title = entry['title'] or "No title" url = entry['url'] visit_count = entry['visit_count'] if browser == "duckduckgo": if is_duckduckgo_encrypted(): # macOS encrypted version has trackers blocked last_visit = cocoa_timestamp_to_datetime(entry['last_visit_time']) trackers_blocked = entry.get('trackers_blocked', 0) output.append(f"{i}. {title}") output.append(f" URL: {url}") output.append(f" Visits: {visit_count} | Trackers blocked: {trackers_blocked} | Last visited: {last_visit}") output.append("") else: # Windows Chromium version last_visit = chrome_timestamp_to_datetime(entry['last_visit_time']) output.append(f"{i}. {title}") output.append(f" URL: {url}") output.append(f" Visits: {visit_count} | Last visited: {last_visit}") output.append("") elif browser == "safari": last_visit = safari_timestamp_to_datetime(entry['last_visit_time']) output.append(f"{i}. {title}") output.append(f" URL: {url}") output.append(f" Visits: {visit_count} | Last visited: {last_visit}") output.append("") elif browser == "firefox": last_visit = firefox_timestamp_to_datetime(entry['last_visit_time']) output.append(f"{i}. {title}") output.append(f" URL: {url}") output.append(f" Visits: {visit_count} | Last visited: {last_visit}") output.append("") else: last_visit = chrome_timestamp_to_datetime(entry['last_visit_time']) output.append(f"{i}. {title}") output.append(f" URL: {url}") output.append(f" Visits: {visit_count} | Last visited: {last_visit}") output.append("") return "\n".join(output) @mcp.tool def get_most_visited(limit: int = 20, browser: Literal["brave", "safari", "chrome", "firefox", "edge", "arc", "opera", "duckduckgo"] = "brave") -> str: """ Get the most frequently visited sites from browser history. Args: limit: Maximum number of results to return (default: 20, max: 100) browser: Which browser to query ("brave", "safari", "chrome", "firefox", "edge", "arc", "opera", or "duckduckgo") Returns: Formatted list of most visited sites with visit counts """ if limit > 100: limit = 100 if browser == "duckduckgo": if is_duckduckgo_encrypted(): # DuckDuckGo macOS - encrypted database schema query = """ SELECT ZURLENCRYPTED, ZTITLEENCRYPTED, ZNUMBEROFTOTALVISITS, ZNUMBEROFTRACKERSBLOCKED, ZLASTVISIT FROM ZHISTORYENTRYMANAGEDOBJECT WHERE ZURLENCRYPTED IS NOT NULL AND ZNUMBEROFTOTALVISITS > 1 ORDER BY ZNUMBEROFTOTALVISITS DESC LIMIT ? """ results = query_duckduckgo_db(query, (limit,)) else: # DuckDuckGo Windows - Chromium-based query = """ SELECT url, title, visit_count, last_visit_time FROM urls WHERE visit_count > 1 AND url NOT LIKE 'https://static.ddg.local/%' ORDER BY visit_count DESC LIMIT ? """ results = query_history_db(query, (limit,), browser) elif browser == "safari": # Safari database schema query = """ SELECT history_items.url as url, history_visits.title as title, COUNT(history_visits.id) as visit_count, MAX(history_visits.visit_time) as last_visit_time FROM history_items JOIN history_visits ON history_items.id = history_visits.history_item GROUP BY history_items.url HAVING visit_count > 1 ORDER BY visit_count DESC LIMIT ? """ elif browser == "firefox": # Firefox database schema (places.sqlite) query = """ SELECT url, title, visit_count, last_visit_date as last_visit_time FROM moz_places WHERE hidden = 0 AND visit_count > 1 ORDER BY visit_count DESC LIMIT ? """ else: # Chromium-based browsers (Brave/Chrome/Edge) database schema query = """ SELECT url, title, visit_count, last_visit_time FROM urls WHERE visit_count > 1 ORDER BY visit_count DESC LIMIT ? """ # Query databases (DuckDuckGo already queried above) if browser != "duckduckgo": results = query_history_db(query, (limit,), browser) if not results: return f"No frequently visited sites found in {browser.capitalize()}" output = [f"Top {len(results)} most visited {browser.capitalize()} sites:\n"] for i, entry in enumerate(results, 1): title = entry['title'] or "No title" url = entry['url'] visit_count = entry['visit_count'] if browser == "duckduckgo": if is_duckduckgo_encrypted(): # macOS encrypted version has trackers blocked last_visit = cocoa_timestamp_to_datetime(entry['last_visit_time']) trackers_blocked = entry.get('trackers_blocked', 0) output.append(f"{i}. {title}") output.append(f" URL: {url}") output.append(f" Visits: {visit_count} | Trackers blocked: {trackers_blocked} | Last visited: {last_visit}") output.append("") else: # Windows Chromium version last_visit = chrome_timestamp_to_datetime(entry['last_visit_time']) output.append(f"{i}. {title}") output.append(f" URL: {url}") output.append(f" Visits: {visit_count} | Last visited: {last_visit}") output.append("") elif browser == "safari": last_visit = safari_timestamp_to_datetime(entry['last_visit_time']) output.append(f"{i}. {title}") output.append(f" URL: {url}") output.append(f" Visits: {visit_count} | Last visited: {last_visit}") output.append("") elif browser == "firefox": last_visit = firefox_timestamp_to_datetime(entry['last_visit_time']) output.append(f"{i}. {title}") output.append(f" URL: {url}") output.append(f" Visits: {visit_count} | Last visited: {last_visit}") output.append("") else: last_visit = chrome_timestamp_to_datetime(entry['last_visit_time']) output.append(f"{i}. {title}") output.append(f" URL: {url}") output.append(f" Visits: {visit_count} | Last visited: {last_visit}") output.append("") return "\n".join(output) if __name__ == "__main__": mcp.run()

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/ronantakizawa/search-history-mcp'

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