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

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
resultYes

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)
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It describes the search behavior, return format, and parameter defaults, but does not address potential limitations like performance, privacy implications, or error handling. It adds value beyond the schema but could be more comprehensive for a tool accessing browser history.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured and front-loaded with the core purpose, followed by organized sections for Args and Returns. Every sentence adds value without redundancy, and the formatting enhances readability. It efficiently conveys necessary information in a compact form.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's moderate complexity (3 parameters, no annotations, but with output schema), the description is mostly complete. It covers purpose, parameters, and return format, but lacks details on behavioral aspects like error cases or performance. The output schema existence reduces the need to explain return values in depth, but some operational context is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema description coverage is 0%, so the description must compensate fully. It provides detailed semantics for all three parameters: 'search_term' (text to search in URLs/titles), 'limit' (default and max values), and 'browser' (enum options with default). This adds significant meaning beyond the basic schema types and enums.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose with a specific verb ('search'), resource ('browser history'), and scope ('URLs and titles containing the search term'). It distinguishes from sibling tools like 'get_most_visited' and 'get_recent_history' by emphasizing search functionality rather than retrieval of predefined categories.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies usage context by specifying search capabilities, but does not explicitly state when to use this tool versus alternatives like 'get_most_visited' or 'get_recent_history'. It provides clear parameter defaults and constraints, which aids usage decisions, but lacks direct sibling tool comparisons or exclusion criteria.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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