Skip to main content
Glama
ronantakizawa

Search History MCP Server

search_history

Search browser history across multiple browsers to find previously visited pages by URL or title. Retrieve formatted results with visit times for personalized data access.

Instructions

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

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
search_termYes
limitNo
browserNobrave

Implementation Reference

  • The core handler function for the 'search_history' tool. It supports searching history across 8 browsers, handles platform differences, database encryption (DuckDuckGo macOS), custom SQL schemas per browser, timestamp conversions, and returns a formatted list of results.
    @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)
  • Type annotations and documentation string defining the input schema (parameters with types and descriptions) and output format for the 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
  • mcp_server.py:465-465 (registration)
    FastMCP decorator that registers the search_history function as a tool.
    @mcp.tool
  • Critical helper function to resolve the correct history database file path for any supported browser and operating system.
    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
  • Helper to safely execute SQL queries on browser history databases by using temporary copies to prevent file locking issues.
    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)

Latest Blog Posts

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